A Code Quality Toolbox for Python Projects
Tags: python, programming, devops, codequality
Published: 2021-08-03 10:33:36

When I started shifting more and more towards working with Python, I looked back at the world of JS and thought wow, they have so many nice tools for formatting and linting! I found myself in the wild world of Python programming where [PEP8](https://www.python.org/dev/peps/pep-0008/) sets the standards, but whether or not you follow them and how you write your code is pretty much up to you. This is my letter to myself back in time, to share some of the tips I now have in my toolbox for maintaining a nice and clean Python project.

## Formatting
If you want to speed up your coding and rely on a computer to do the polishing for you, you can use a code formatter to automatically walk through and format your code. In the world of python, [Black](https://github.com/psf/black) is one of the most used formatters. It could be compared to [Prettier](https://prettier.io/) in the world of JS - they are both opinionated formatters, i.e. they make some choices for you. (You don't have to fight your colleague over whether to use single or double quotes anymore! Great!) 

This is how black responds to a file with `print("hello" )`.
```
$ pip install black
$ black my-sample.py
reformatted my-sample.py
All done! ✨ 🍰 ✨
1 file reformatted.
```

## Linting
Linting differs from formatting in that it will not change the code it looks at, but will notify you if it doesn't follow the specified standards. A good option for linting in python is [flake8](https://flake8.pycqa.org/en/latest/). Here is how flake8 responds to the same file contents as black did above.
```
$ pip install flake8
$ flake8 my-sample.py
my-sample.py:1:14: E202 whitespace before ')'
```
In addition to this out of the box -linting, there are loads of [flake8 extensions](https://github.com/DmytroLitvinov/awesome-flake8-extensions) that can help you with for example switching from `.format()` to using f-strings or checking that your naming follows the PEP8 guidelines. For example, adding `flake8-length` adds line length checking to the linting.
```
$ pip install flake8-length
$ flake8 test.py
test.py:1:80: LN001 line is too long (169 > 79)
```
If you like to keep your imports in tact, there are also rules to enforce a specific order amongst them. One tool that does that is [isort](https://pycqa.github.io/isort/). Isort follows the order:
1. absolute imports
2. built-in modules
3. third-party libraries
4. imports from the same project
You can also integrate isort into flake8 using the [flake8-isort](https://github.com/gforcada/flake8-isort) plugin! 

## Type checking
Type annotation support was introduced in python 3.5 and there are tools for watching that the typing is in tact. A good option for checking your type annotations is using [mypy](http://mypy-lang.org/). Given a sample file that looks like this:
```
def return_hello(name: str) -> int:
    return f'Hello, {name}!'
```
We can run mypy and see that our type hints indeed are off:
```
$ pip install mypy
$ mypy my-sample.py
my-sample.py:2: error: Incompatible return value type (got "str", expected "int")
Found 1 error in 1 file (checked 1 source file)
```
Some other good alternatives for type checking are [Pyright](https://github.com/microsoft/pyright) (which is seen in VSCode via Microsofts PyLance plugin) and [Pyre](https://pyre-check.org/).

## Wiring it up
You could choose to manually run these tools from time to time in your project, but you will have a better time if you make them part of your pull request routine. Using GitHub actions, you can set up for example a [flake8 action](https://github.com/marketplace/actions/flake8-action). You can alternatively build an action from scratch - here we run isort with the check-only option:

```yaml
name: Check import order

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Set up Python 3.9
      uses: actions/setup-python@v1
      with:
        python-version: 3.9
    - name: Install isort
      run: |
        python -m pip install --upgrade pip
        pip install isort
    - name: Check import order
      run: isort . -c -v
```

## Final thoughts
I love rules, so seeing how systematically one can unify ones codebase using tools like these just makes my day. However I also know lots of devs who don't feel like this kind of rigour is their jam - it should always be discussed in the team how and why we want to set the codebase standards. 

Hope this text gave you all some ideas of what tools are out there and what you could pick into your own toolkit as well! If you know of a useful tool I didn't mention - please let me know in the comments! 😊