Static Code Analysis for Python. Type and style checking, potential... | by Martin Thoma | Towards Data Science

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.
One way to measure code complexity is the cyclomatic complexity, also called McCabe complexity as defined in A Complexity Measure:
CC = E - N + 2*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 condition-nodes (if-statements, while/for loops).
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: Nudge you to name complex types
flake8-cognitive-complexity: Validates cognitive functions complexity
flake8-expression-complexity: Make sure that single expressions are not too complicated; similar to cyclomatic complexity for functions / classes.
flake8-functions: Report too long functions 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, reporting on the complexity of Python tests and applications.
xenon: Relies on radon
You might have heard the words “pythonic code”. It means to not only write correct Python code but use the languages features how 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 which I’ve seen most people not to agree with is the maximum line length of 79. I’m always recommending to follow this advice in 95% of your codebase. I gave reasons for that.
For pure code formatting, you should use an auto formatter. I grew into liking 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 auto formatter for Python.
yapf by Google is another auto formatter.
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 markdown with some specified sections such as Parameters and Returns in a fixed order.
Google-style docstrings: A super-slim format which has Args: and Returns: .
Sphinx-style docstrings: A very flexible format that uses restructured text.
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    """
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: Typo squatting 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 to be used. William Bengtsson also did something similar to harden the Python community against this thread. See his article below for more information about his project. Package parkinginflates the number of packages on PyPI and I filtered them by looking for the summary “A package to prevent exploit”.
medium.com
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 Pokemon 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 prospector. It couples tools, but it is way less commonly used and thus not as flexible as Flake8.
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 request framework
It happened quite a couple of times to me: 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.
pylint is one of the most wide-spread linters in Python. The features of pylint for sure overlaps with Flake8, but there is one feature I love: Checking for code duplication ❤
$ pylint --disable=all --enable=duplicate-code .************* Module mpu.datastructures.trie.basempu/datastructures/trie/base.py:1:0: R0801: Similar lines in 2 files==mpu.datastructures.trie.char_trie:85==mpu.datastructures.trie.string_trie:138            string += child.print(_indent=_indent + 1)        return stringdef __str__(self):        return f"TrieNode(value='{self._value}', nb_children='{len(self.children)}')"__repr__ = __str__EMPTY_NODE = TrieNode(value="", is_word=False, count=0, freeze=True)class Trie(AbstractTrie):    def __init__(self, container=None):        if container is None:            container = [] (duplicate-code)
Who hasn’t done it: You removed a functionality, but the code could be handy. So you comment it out. Or you add a 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
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 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-sts: 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 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.
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.
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 mpuFound mpu------------------------------Final rating: 10/10Your cheese is so fresh most pe$ pyroma nox------------------------------Checking noxFound 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/10Cottage Cheese------------------------------
In this series, we already had:
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
Part 10: Pytest Plugins to Love
Let me know if you’re interested in other topics around testing with Python.