A tool for imposing architectural constraints on your Python projects.
Python is a wonderful language that is a joy to develop in. But I’ve found that projects written in Python can easily grow into an unmaintainable mess. Keeping a code base maintainable, particularly when it’s large and complex, is difficult.
A common strategy is to organise it into smaller, decoupled subpackages. But that’s easier said than done. Circular dependencies between these subpackages have a nasty habit of creeping in. Over time, what you worked hard to separate creeps inexorably together.
Part of the problem is that Python has no formal way of declaring, and enforcing, a dependency flow. That’s why I wrote Layer Linter.
What Layer Linter does
Layer Linter is a tool that helps impose a structure on your Python project, based on its internal dependency flows. It analyses which modules are importing which, and checks this conforms to a contract defined by you.
In this contract, you describe an ordered list of layers. Each layer is just a subpackage or module within your codebase. The contract stipulates that any code within a layer lower down the list must not import, even indirectly, anything from a higher up layer.
For example, you could decide to structure your project using three layers:
interfaces
(highest level)domain
data
(lowest level)
Once you’ve created the contract, you run Layer Linter’s command line tool to see if anything is not adhering to your architecture. Here’s what the output might look like:
$ layer-lint myproject ============ Layer Linter ============ --------- Contracts --------- Three tier architecture BROKENContracts: 0 kept, 1 broken. ---------------- Broken contracts ---------------- Three tier architecture ----------------------- 1. myproject.data.userrecord imports myproject.domain.user: myproject.data.userrecord <- myproject.domain.user 2. myproject.domain.user imports myproject.interfaces.api: myproject.domain.user <- myproject.common.apitools <- myproject.interfaces.api
In this report, Layer Linter is telling you that you’re not
adhering to your architecture in two places. First, the data
layer
(which is the lowest level) is importing from domain
(which is a mid level layer).
Second, domain
is importing (via a package not listed in the contract) something
from interfaces
(the highest layer).
If you’re serious about preventing violations of the contract, you can add
the layer-lint
command to your automated test / continuous integration run. (You can
see an example of this in the Layer Linter repo.)
Usage
To use Layer Linter, first you need to define your layers. You do this in a layers.yml
file that looks something like this:
My contract: packages: myproject layers: - high_level - medium_level - low_level
You then run:
$ layer-lint myproject
The report will tell you whether you’re following your contract.
You can define other architectural styles too, such as a more modular layered style which I call the ‘Rocky River’. It also supports multiple contracts. See more detailed information about how to use Layer Linter in the docs.
(Note: Since this post was written, Layer Linter has been superseded by Import Linter, which does everything Layer Linter does, but with more features and a slightly different API. There is a guide to the minor API differences here.)
Further information
- Layer Linter documentation.
- The Layers pattern in Chapter 2 of Pattern-Oriented Software Architecuture Vol. 1 (Buschmann, Meunier, Rohnert, Sommerlad and Stal, 1996).
- My talk on layers and the Rocky River pattern.
- Encapsulated Django.
Comments