Essential developer tools for improving your Python code
By: Chris Bergman
Tags: Pytest, Tools, Code Quality, Python, Software Development

The tools

Project configuration

Of course some configuration is needed to align all tools and in- or exclude some parameters. Every tool has its own set of configuration parameters and configuration formats, however there is a trend where tools are conforming to the pyproject.toml format (see PEP 518 for more information). For every tool below I’ll mention relevant configuration parameters in a pyproject.toml section whenever possible. As a bonus I’m putting the final pyproject.toml in a Github repository for your convenience.

*note: the ‘app’ folder in the examples below refers to my project folder.

Code style and formatting

For code style and formatting I am using isort, Flake8, Bugbear, Black and Mypy. I’ll discuss them in detail below.

isort

Github repository

What does it do?

It sorts your import statements from this:

from pytz import timezone

from app.const import NAME



from datetime import datetime as dt

to this:

# Standard library imports

from datetime import datetime as dt



# Third party imports

from pytz import timezone



# Local application imports

from app.const import NAME

Pretty neat huh?

Configuration

[tool.isort]

multi_line_output = 3

include_trailing_comma = true

force_grid_wrap = 0

use_parentheses = true

line_length = 88

skip = []

import_heading_stdlib = 'Standard library imports'

import_heading_thirdparty = 'Third party imports'

import_heading_firstparty = 'Local application imports'

import_heading_localfolder = 'Local folder imports'

known_first_party = ['app']

How to install and run?

pip install isort

isort .

Flake8

Github repository

What does it do?

Flake8 is a wrapper around pyflakes, pycodestyle and Ned Batchelder’s McCabe script essentially checking for conformance to the PEP 8 code style standard.

If any non-conformances are detected the tool will output a warning with an error code. Here are some examples of the errors it outputs:

app/main.py:14:7: E111 indentation is not a multiple of four

app/main.py:14:7: E117 over-indented

app/main.py:15:7: E111 indentation is not a multiple of four

app/const.py:1:20: W292 no newline at end of file

Note: Several plugins exist for flake8 extending its functionality (including for some of the other tools mentioned here e.g. ‘flake8-isort’) however I prefer to keep the execution of these tools separate which makes configuration and execution easier.

Configuration

Flake8 doesn’t support pyproject.toml configuration yet so we need to create a .flake8 configuration file in the project root instead:

[flake8]

ignore = E203, E266, E501, W503

max-line-length = 88

max-complexity = 18

select = C,E,F,W,B

filename = app/*.py

Note: the B select is for Bugbear error codes.

How to install and run?

pip install flake8

flake8 <project_folder_name>

Bugbear

Github repository

What does it do?

Bugbear is a plugin for flake8 containing some rules for errors and warnings that do not fit within pyflakes or pycodestyle. An example of this is rule B009:

Do not call getattr(x, 'attr') , instead use normal property access: x.attr . Missing a default to getattr will cause an AttributeError to be raised for non-existent properties. There is no additional safety in using getattr if you know the attribute name ahead of time.

Configuration

Just adding the B to the select configuration parameter of flake8 should suffice.

How to install and run?

pip install flake8-bugbear

flake8 <project_folder_name>

Black

Github repository

What does it do?

Black is an (awesome) opinionated code formatter adhering to PEP8 (as much as possible).

It turns this:

def print_hi(name):

date_time = tz.localize(

dt.now())

print(

f"Hi, {name} it is currently {date_time}"

)

def print_hello(

name

):

print(

f"Hello, {name} "

f"how are you?"

)

into this:

def print_hi(name):

date_time = tz.localize(dt.now())

print(f"Hi, {name} it is currently {date_time}")





def print_hello(name):

print(f"Hello, {name} " f"how are you?")

✨ Magic! ✨

Configuration

[tool.black]

line-length = 88

target-version = ['py38']

include = '\.pyi?$'

exclude = '''

/(

\.eggs # exclude a few common directories in the

| \.git # root of the project

| \.hg

| \.mypy_cache

| \.tox

| \.venv

| _build

| buck-out

| build

| dist

| migrations



# The following are specific to Black, you probably don't want those.

| blib2to3

| tests/data

)/

'''

How to install and run?

pip install black

black <project_folder_name>

Mypy

Github repository

What does it do?

Using Mypy you can add type hinst (PEP 484) to your code and check your code for type errors.

Fun fact: Jukka Lehtosalo, the creator of Mypy, worked together with the creator of Python (Guido van Rossum) on Mypy at Dropbox. More info here.

Examples of such type errors:

app/main.py:15: error: Missing return statement

app/main.py:25: error: Argument 1 to "print_hi" has incompatible type "str"; expected "List[str]"

Found 2 errors in 1 file (checked 3 source files)

Configuration

Mypy doesn’t support pyproject.toml yet, so we have to create a mypy.ini file in the project root containing:

[mypy]

python_version=3.8

platform=linux



files=app/

ignore_missing_imports=true

How to install and run?

pip install mypy

mypy <project_folder_name>

Software testing

For software testing I am using Pytest and coverage.py.

Pytest

Github repository

What does it do?

Pytest is a framework for software testing Python code. One of its features I like best is that it allows you to create re-usable pieces of test code (fixtures) that can be combined in a single test.

Configuration

Pytest has support for pyproject.toml configuration:

[tool.pytest.ini_options]

minversion = "6.0"

addopts = "-ra"

testpaths = [

"app",

]

python_files = "*_test.py"

junit_family = "legacy"

note: any file with a name ending with _test.py will be automatically tested when running pytest.

How to install and run?

pip install pytest

pytest <project_folder_name>

Output of a successful test:

➜ pytest app -ra

===================== test session starts ========================

platform darwin -- Python 3.8.5, ..., pluggy-0.13.1

rootdir: /, configfile: pyproject.toml, testpaths: app

collected 1 item app/main_test.py . [100%] ===================== 1 passed in 0.05s ==========================

Output of a failed test:

pytest app -ra

===================== test session starts ========================

platform darwin -- Python 3.8.5, ..., pluggy-0.13.1

rootdir: /, configfile: pyproject.toml, testpaths: app

collected 1 item app/main_test.py F [100%] =========================== FAILURES ==============================

________________________ test_print_hi ____________________________ capsys = <_pytest.capture.CaptureFixture object at 0x1056d5fa0> def test_print_hi(capsys):

from app.main import print_hi # Happy flow

name = "Chris"

print_hi(name)

captured = capsys.readouterr()

assert captured.out.startswith(f"Hi, {name} it is currently") # Invalid type

name = 1

> print_hi(name) app/main_test.py:16:

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ name = 1 def print_hi(name: str) -> str:

if not isinstance(name, str):

> raise TypeError("name is of invalid type")

E TypeError: name is of invalid type app/main.py:17: TypeError

===================== short test summary info =====================

FAILED app/main_test.py::test_print_hi - TypeError: name is of invalid type

======================= 1 failed in 0.07s =========================

Coverage.py

Github repository

What does it do?

Using coverage.py you can measure the % of code executed, for example when running pytest. These metrics give you an indication how much of your code is covered by software tests. Of course, the coverage percentage does not indicate the quality of software testing.

Configuration

[tool.coverage.run]

branch = true

command_line = '-m pytest'

source = ['app/']



[tool.coverage.report]

precision = 2

skip_covered = true

fail_under = 90

How to install and run?

pip install coverage[toml]

coverage run

coverage report -m

Example output of the coverage report:

coverage report -m

Name Stmts Miss Branch BrPart Cover Missing

------------------------------------------------------------

app/main.py 14 1 6 1 90.00% 24->25, 25

------------------------------------------------------------

TOTAL 28 1 6 1 94.12% 3 files skipped due to complete coverage.

Security

For checking security vulnerabilities I am using Bandit and Safety.

Bandit

Github repository

What does it do?

Running Bandit on your code enables you to identify known security issues (e.g. the use of assert statements as checks which are removed when compiling to byte code). A listing of all checks can be found here.

Configuration

Bandit does not support the pyproject.toml yet so to configure it one has to create a .bandit file in the project folder root.

[bandit]

targets = app/

recursive = true

skips = B101

Note: the B101 test is skipped because it (also) triggers on assert statement used in _test.py files. There is a request to ignore B101 when the assert is in a test file.

How to install and run?

pip install bandit

bandit --ini .bandit -r

Example output:

bandit --ini .bandit -r

[main] INFO Using ini file for skipped tests

[main] INFO Using ini file for selected targets

[main] INFO profile include tests: None

[main] INFO profile exclude tests: None

[main] INFO cli include tests: None

[main] INFO cli exclude tests: B101

[main] INFO running on Python 3.8.5

Run started:2020-11-29 15:27:27.049118 Test results:

No issues identified. Code scanned:

Total lines of code: 28

Total lines skipped (#nosec): 0 Run metrics:

Total issues (by severity):

Undefined: 0.0

Low: 0.0

Medium: 0.0

High: 0.0

Total issues (by confidence):

Undefined: 0.0

Low: 0.0

Medium: 0.0

High: 0.0

Files skipped (0):

Safety

Github repository

What does it do?

It scans your installed dependencies for known security vulnerabilities. By default it uses the safety-db database with known vulnerabilities.

Configuration

No detailed configuration needed for running Safety.

How to install and run?

pip install safety

safety check

Example output: