Linter for Python Architecture
The goal of the architecture is to organize and constrain the dependencies between the components of your project. For example, in a regular Django project, views depend on models, and not vise versa. If you have utility functions, they shouldn’t depend on any specificities of your project.
I was playing with import-linter, and it looks promising.
What import-linter does
In Python projects, we define dependencies by imports. If module A imports module B, then A depends on B. The import-linter lets you specify the rules that declaratively constrain that dependency flow. In a config file, you define so-called contracts. For example, one contract can say, “in my projects, models.py must not have any imports from the views.py.”
[importlinter] root_package = myproject [importlinter:contract:models_views] name = Models don't import views type = forbidden source_modules = myproject.views forbidden_modules = myproject.models
If your models.py contains something like
from . import views, the linter raises an error.
At the moment, there are three types of contracts:
- Forbidden modules: checks that one set of modules are not imported by another set of modules.
- Independence: checks that a set of modules do not depend on each other.
- Layers: enforces a layered architecture, where higher layers may depend on lower layers, but not the other way around.
If you can’t express your architecture with these contracts, or you find yourself writing too many similar rules, you can write yours, and it’s straightforward.
How import-linter works
I love the elegance of the model behind the import-linter. To analyze the project, it builds an import graph. Your contract takes it and checks if any edges violate the contact rules. Is there an edge between module X and module Y? Are there any loops? Is there a path from X to Y, etc.?
Kind of summary
Things that I like:
- There is no magic in the project, and the graph-based model is both transparent and powerful.
- It’s quite fast even on large projects.
- It doesn’t enforce defining the architecture for the entire application. On a legacy project, you can start small.
- You can define your own architecture rules with a Python class, and it’s straightforward.
- Works with pre-commit.
Things that left me confused or slightly disappointed:
- It looks like a limited set of contract types makes you write different contracts for something that logically should be one contract.
- Documentation uses generic like “foo” or “project_one”. Opinionated examples for a simple CRUD application or a typical Django or Flask project would help a lot.
I am still at the early stage of adoption but already added it to my Python cookiecutter. Let’s see if it sticks.