Improving your python code quality with automatic architecture contract validation


If you care about code quality you are probably already familiar with linting-tools like flake8 and pylint. These tools automatically check your python code for common code-smells and anti-patterns.

Another excellent tool in this category is import-linter. Import-linter can check that your dependency tree adheres to your chosen architecture. For example, in a Django project, it would be bad practice if your model would import a view or a form. With import-linter, you can specify this as a layer-contract:

[importlinter:contract:1]
name=Lower layers (e.g. models) are not allowed to import higher layers (e.g. forms)
type=layers
containers=
    your_main_app
layers=
    views
    forms
    models

This contract specifies that

  • you have an app-module your_main_app
  • inside that app-modules there are three layers, from the lowest to the highest: models, forms, views
  • lower layers are not allowed to depend on (i.e. import from) higher layers

Your project folder would look something like this:

project
├── .importlinter
└── my_main_app
    ├── __init__.py
    ├── forms.py
    ├── models.py   # we'll add a "wrong" import here in the next step
    └── views.py

When we run lint-imports we will see that our contract is intact:

$ lint-imports
=============
Import Linter
=============
---------
Contracts
---------
Analyzed 4 files, 0 dependencies.
---------------------------------
Lower layers (e.g. models) are not allowed to import higher layers (e.g. forms) KEPT
Contracts: 1 kept, 0 broken.

Let’s break the contract

Let’s say you accidentally violate that self-imposed contract by having from .views import MyView in your models.py. Once you run lint-imports, you will get the following error message:

$ lint-imports
=============
Import Linter
=============
---------
Contracts
---------
Analyzed 4 files, 1 dependencies.
---------------------------------
Lower layers (e.g. models) are not allowed to import higher layers (e.g. forms) BROKEN
Contracts: 0 kept, 1 broken.
----------------
Broken contracts
----------------
Lower layers (e.g. models) are not allowed to import higher layers (e.g. forms)
----------------------------------------------------
my_main_app.models is not allowed to import my_main_app.views:
- my_main_app.models -> my_main_app.views (l.1)

Excellent. lint-imports caught the problem and pointed out the violating module and the line of code.

In addition to the “layer” contract type there are also “forbidden”, “independent”, and even a “custom” contract type. Check it out!

I can highly recommend adding import-linter to your automatic python code quality checks in your CI pipeline and your git pre-commit hooks.

For completeness, here is the full .importlinter config file used in this example:

[importlinter]
root_packages =
    my_main_app

[importlinter:contract:1]
name=Lower layers (e.g. models) are not allowed to import higher layers (e.g. forms)
type=layers
layers=
    views
    forms
    models
containers=
    my_main_app

See also