Static code analysis looks at the code without executing it. It is usually extremely fast to execute, requires little effort to add to your workflow, and can uncover common mistakes. The only downside is that it is not tailored towards your code.
In this article, you will learn how to perform various types of static code analysis in Python. While the article focuses on Python, the types of analysis can be done in any programming language.
Code Complexity
Photo by John Barkiple on Unsplash
One way to measure code complexity is the cyclomatic complexity, also called McCabe complexity as defined in A Complexity Measure:
$$CC = E - N + 2 \cdot P$$
where N is the number of nodes in the control flow graph, E is the number of edges, and P is the number of connected components (P = 1 for a single function).
You can calculate it in Python with radon:
$ pip install radon
$ radon cc mpu/aws.py -s
mpu/aws.py
F 85:0 s3_download - B (6)
F 16:0 list_files - A (3)
F 165:0 _s3_path_split - A (2)
F 46:0 s3_read - A (1)
F 141:0 s3_upload - A (1)
C 77:0 ExistsStrategy - A (1)
The first letter shows the type of block (F for function, C for class). Then radon gives the line number, the name of the class/function, a grade (A, B, C, D, E, or F), and the actual complexity as a number. Typically, a complexity below 10 is OK. The most complex part of SciPy has a complexity of 61.
Besides radon, there are various other packages and Flake8 plugins:
- flake8-annotations-complexity: Nudges you to name complex types
- flake8-cognitive-complexity: Validates the cognitive complexity of functions
- flake8-expression-complexity: Makes sure that single expressions are not too complicated; similar to cyclomatic complexity for functions / classes.
- flake8-functions: Reports functions that are too long and functions with too many arguments
- mccabe: This is used by a couple of other tools and projects
- wily: A command-line application for tracking and reporting on the complexity of Python tests and applications.
- xenon: Relies on radon
Style Guides
Make your code look professional. Photo by Hunters Race on Unsplash
You might have heard the words “pythonic code”. It means to not only write correct Python code but also use the language's features as they are intended to be used (source). It is for sure an opinionated term, but there are a lot of plugins that show you what a large part of the community considers to be pythonic.
Writing code in a similar style to other Python projects is valuable as people will have an easier time reading the code. This is important as we read software more often than we write it (source).
So, what is pythonic code?
Let’s start with PEP-8: It’s a style guide written and accepted by the Python community in 2001. So it’s been around for a while and most people want to follow most of it. The main part I’ve seen most people disagree with is the maximum line length of 79. I always recommend following this advice in 95% of your codebase. I gave reasons for that.
For pure code formatting, you should use an autoformatter. I grew to like black because it does NOT allow customization. Code formatted by black always looks the same. As you cannot customize it, you don’t need to discuss it. It just solves the issue of conflicting styles and arguments around it. Black is maintained by the Python Software Foundation and likely the most commonly adopted autoformatter for Python.
yapf by Google is another autoformatter.
Docstrings
Reading the manual can be fun if it’s written well. Lasagne and SciPy have pretty good documentation. Photo by Laura Dewilde on Unsplash
For docstrings, there is PEP-257. All of those rules are widely accepted in the community, but they still allow a wide variety of docstrings. There are three commonly used styles:
- NumpyDoc-style docstrings: Used by NumPy and SciPy. It’s reStructuredText with some specified sections such as
ParametersandReturnsin a fixed order. - Google-style docstrings: A super-slim format which has
Args:andReturns:. - Sphinx-style docstrings: A very flexible format that uses reStructuredText.
I love the NumpyDoc format as it is super easy to read even when you just have it inside a text editor. Numpydoc is also well-supported by editors.
Here you can see the three in comparison:
def get_meta_numpydoc(filepath, a_number, a_dict):
"""
Get meta-information of an image.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aenean commodo
ligula eget dolor. Aenean massa. Cum sociis natoque penatibus
et magnis dis
parturient montes, nascetur ridiculus mus.
Parameters
----------
filepath : str
Get metadata from this file
a_number : int
Some more details
a_dict : dict
Configuration
Returns
-------
meta : dict
Extracted meta information
Raises
------
IOError
File could not be read
"""
def get_meta_google_doc(filepath, a_number, a_dict):
"""Get meta-information of an image.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aenean commodo
ligula eget dolor. Aenean massa. Cum sociis natoque penatibus
et magnis dis
parturient montes, nascetur ridiculus mus.
Args:
filepath: Get metadata from this file.
a_number: Some more details.
a_dict: Configuration.
Returns:
Extracted meta information:
Raises:
IOError: File could not be read.
"""
def get_meta_sphinx_doc(filepath, a_number, a_dict):
"""
Get meta-information of an image.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aenean commodo
ligula eget dolor. Aenean massa. Cum sociis natoque penatibus
et magnis dis
parturient montes, nascetur ridiculus mus.
:param filepath: Get metadata from this file
:type filepath: str
:param a_number: Some more details
:type a_number: int
:param a_dict: Configuration
:type a_dict: dict
:returns: dict -- Extracted meta information
:raises: IOError
"""
Flake8
You should always use a linter, as Alberto Gimeno pointed out. They can check your style, but more importantly, show potential errors.
Flake8 is a wrapper around PyFlakes, pycodestyle, and a McCabe script. It is the most commonly used tool for linting in Python. Flake8 is awesome because there are so many plugins for it. I found 223 packages with the string “flake8” within the name and looked at many of them. I’ve also looked at packages with the trove classifier Framework :: Flake8 and found 143 packages, of which 122 started with flake8-. Only 21 packages had the Flake8 Framework trove classifier but didn’t start with flake8-, and only two of them looked interesting.
Side note: Typosquatting is an issue every open package repository has to fight with (Bachelor’s Thesis: Typosquatting in Programming Language Package Managers which has a blog post and an interesting follow-up, Bachelor’s Thesis: Attacks on Package Managers). There are examples in Python for it causing harm (2017, 2017, 2017, 2019, 2019, 2019). There is pypi-scan for finding examples and pypi-parker to prevent common typos from being used. William Bengtson also did something similar to harden the Python community against this threat. See his article below for more information about his project. Package parking inflates the number of packages on PyPI and I filtered them by looking for the summary “A package to prevent exploit”. Python Typosquatting for Fun not Profit
Here are some of the interesting flake8 plugins:
- cohesion: Check if class cohesion is below a threshold. This indicates that functionality should be split out of a class.
- flake8-assert-msg: Make sure assert statements have messages
- flake8-blind-except: Prevent Pokémon exception catching
- flake8-builtins: Check for python builtins being used as variables or parameters.
- flake8-docstrings: Adds pydocstyle support
- flake8-isort: Use isort to check if the imports on your python files are sorted the way you expect
- flake8-logging-format: Validate (lack of) logging format strings
- flake8-pytest-style: Checking common style issues or inconsistencies with pytest-based tests
- flake8-requirements: Checks/validates package import requirements. It reports missing and/or not used project direct dependencies
- flake8-graphql: Lint GraphQL query strings
- flake8_implicit_str_concat: Goes well with black 🎉
- flake8-mock: Check for mistakes using mocks
- flake8-nb: Check Jupyter notebooks
- flake8-pyi: Lint stub files
- flake8-variables-names: Find common “meaningless” variable names
- pep8-naming: Check your code against PEP 8 naming conventions
- pandas-vet: Opinionated linting for Pandas code
- wemake-python-styleguide: An opinionated style guide/checker which seems to be pretty popular. I haven’t seen that one before, though.
An alternative to parts of Flake8 is prospector. It combines tools, but it is way less commonly used and thus not as flexible as Flake8.
Flake8: Security and Bugs
Be safe by looking at warning signs. Photo by Troy Bridges on Unsplash
- flake8-bandit: Security Testing
- flake8-bugbear: finding likely bugs and design problems in your program — usually it’s silent, but when it’s not you should have a look 🐻
- flake8-requests: checks usage of the requests library
Flake8: Remove Debugging Artifacts
It has happened to me quite a few times: I’ve added some code while developing a new feature or debugging an old one and forgot to remove it afterward. It was most often caught by the reviewer, but it is not necessary to distract the reviewer with this.
flake8-breakpoint checks for forgotten breakpoints and flake8-print will complain about every print statement. flake8-debugger, flake8-fixme, flake8-todo go in the same direction.
Let Dead Code Die
Photo by Kenny Orr on Unsplash
Who hasn’t done it: You removed a functionality, but the code could be handy. So you comment it out. Or you add an if False block around it. Sometimes more sophisticated by adding a configuration option you don’t need.
The clean solution is to have a single, clear commit that removes that feature. Maybe add a git tag so that you can find it later if you want to add it again.
And then there is code which is dead, but you forgot about it. Luckily, you can automatically detect it:
- flake8-eradicate: Find commented-out (or so-called “dead”) code.
- vulture: Finds unused code in Python programs
Flake8: Nudging Yourself to use Good Style
Having an experienced developer review your code is awesome. In the best case, you will learn something new that you can apply in all further projects. And some plugins act like that. Photo by Brooke Cagle on Unsplash
Some plugins helped me to learn something about Python. For example, the following helped me to get rid of small little bugs and inconsistencies:
- flake8-comprehensions: Helps you write better list/set/dict comprehensions — I love this one 😍
- flake8-executable: Check executable permissions and shebangs. Files should either be executable and have a shebang, or not be executable and not have a shebang.
- flake8-raise: Finds improvements for raise statements
- flake8-pytest: Use assert instead of assertEqual
The following new style nudging plugins aim to push you to use modern style Python:
- flake8-pathlib: Pathlib was added in Python 3.4 and I’m still not quite used to it. This plugin might nudge me to use it when it’s appropriate.
- flake8-string-format, flake8-printf-formatting, flake8-sfs: String formatting.
This is one of the most valuable categories for me. If you know more plugins which help to use new styles, let me know 😃
Flake8 Meta Plugins
Image created by Martin Thoma via imgflip.com
Flake8 has some plugins which don’t add more linting functionality, but improve flake8 in another way:
- flake8-colors: ANSI colors highlight for Flake8
- flake8-csv: Generate error reports in CSV format
- flake8-json: Generate error reports in JSON format
- flake8-dashboard and flake8-html: Generate an HTML report (dashboard demo)
- flake8-immediate: Prints the errors directly without any delay
- flake8-strftime: Checks for use of platform-specific strftime codes
- flake8-SQL and py-find-injection: Looks for SQL queries and checks them against an opinionated style
- flake8-tuple: Checks for (probably) unintended one-element tuples
And some plugins people might need for legal reasons like flake8-author, flake8-copyright, and flake8-license.
To Flake8 plugin authors: Please make sure that you list the error codes your plugin introduces and that you give at least some examples of what your plugin considers bad / good.
Type Annotations and Type Checking
The mypy plugin for VS Code showing an issue with the types. Screenshot by Martin Thoma.
It’s possible in Python, but you need to do it. It’s not done automatically. I’ve written a longer article about how type annotations work in Python. There are multiple tools you can use, but I recommend mypy. You can run it via pytest by using pytest-mypy or via flake8 by using flake8-mypy, but I prefer to run it separately. The main reason for it is that the output given by CI pipelines is cleaner.
You can integrate type checking (e.g. via mypy) into your editor, but the type annotations alone already go a long way as they document what is expected.
Package Structure
Check that your package looks fine before shipping it. Photo by Toby Stodart on Unsplash
pyroma rates how well a Python project complies with the best practices of the Python packaging ecosystem.
Here are some examples of my projects:
$ pyroma mpu
------------------------------
Checking mpu
Found mpu
------------------------------
Final rating: 10/10
Your cheese is so fresh most pe
$ pyroma nox
------------------------------
Checking nox
Found nox
------------------------------
Your long_description is not valid ReST:
<string>:2: (WARNING/2) Explicit markup ends without a blank line; unexpected unindent.
<string>:3: (WARNING/2) Field list ends without a blank line; unexpected unindent.
<string>:4: (WARNING/2) Explicit markup ends without a blank line; unexpected unindent.
------------------------------
Final rating: 9/10
Cottage Cheese
------------------------------
More in this series
This article is part of my series about unit testing in Python:
- Part 1: The basics of Unit Testing in Python
- Part 2: Patching, Mocks and Dependency Injection
- Part 3: How to test Flask applications with Databases, Templates and Protected Pages
- Part 4: tox and nox
- Part 5: Structuring Unit Tests
- Part 6: CI-Pipelines
- Part 7: Property-based Testing
- Part 8: Mutation Testing
- Part 9: Static Code Analysis: Linters, Type Checking, and Code Complexity
Let me know if you’re interested in other topics around testing with Python or professional software development with Python: [email protected]