Enforce import rules using the Python import linter
By default, Python modules can import any internal/external Python modules without any restrictions. However, this flexibility can cause maintainability issues, especially in a large codebase, and therefore you might want to impose some import rules.
Here are some use cases.
- You want to automatically prevent a circular import, which is one of the most common import issues.
- You want to keep two or more modules completely independent. It means that they never import each other directly or indirectly, anywhere in the codebase.
- You want to impose restrictions on importing external Python libaries. For example, if you are not allowed to use cloud-related libraries due to company policy etc.
Similar to pylint or flake8, which enforce coding standards, you can restrict how programmers import modules in the codebase using Import Linter. I will show you some brief examples of Import Linter in this post, and will leave the details to the official documentation.
You can find the example code from this post in my Github repository https://github.com/921kiyo/import-linter-example.
Setup
You can set up import-linter
in three steps.
- Pip install
import-linter
.pip install import-linter
-
In the project root, create the
.importlinter
file, where you will specify import rules called contracts. - Run
lint-imports
from Terminal to get the linting result.
Since step 1 is self-explanatory, I will explain steps 2 and 3 with an example below.
Step 2: Setup configuration
Specify the restrictions in the .importlinter
file, which looks like this.
[importlinter]
root_package=project
include_external_packages=True
[importlinter:contract:1]
name=oil and water are independent
type=independence
modules=
project.oil
project.water
[importlinter:contract:2]
name=fire cannot import water (but water can import fire)
type=forbidden
source_modules=
project.fire
forbidden_modules=
project.water
[importlinter:contract:3]
name=Disallow to import boto3 for security reason
type=forbidden
source_modules=
project
forbidden_modules=
boto3
[importlinter:contract:4]
name=Only higher layers can import lower ones. stove (high) -> fire (medium) -> oil (low)
type=layers
layers=
project.stove
project.fire
project.oil
First, you need to specify where the root of the Python package is in root_package
to run Import Linter. Optionally, you can enable validation for external package import.
[importlinter]
root_package=project
include_external_packages=True
After that, include as many contracts as you want for the import rules. In this example, I have included four.
1. Independence contract
The independence contract ensures that the modules never import each other.
[importlinter:contract:1]
name=oil and water are independent
type=independence
modules=
project.oil
project.water
2. Forbidden contract
Similar to the independence contract, you can limit a certain import by specifying source
and forbidden
modules as follows.
[importlinter:contract:2]
name=fire cannot import water (but water can import fire)
type=forbidden
source_modules=
project.fire
forbidden_modules=
project.water
The forbidden
type rule can also be applied to external modules as well.
[importlinter:contract:3]
name=Disallow to import boto3 for security reason
type=forbidden
source_modules=
project
forbidden_modules=
boto3
3. Layer contract
The more structural way of imposing a rule is a layer contract: the modules in the higher layers can depend on the modules in the lower layers, but not the other way around.
[importlinter:contract:4]
name=Only higher layers can import lower ones. stove (high) -> fire (medium) -> oil (low)
type=layers
layers=
project.stove
project.fire
project.oil
In this example, project.stove
can import project.fire
and project.oil
, but project.oil
cannot import project.fire
or project.stove
.
If none of the default contracts satisfies your needs, you can create a custom contract.
Step 3: Running Import Linter
Once .importlinter
config is set up, run lint-imports
in Terminal to get the linting result.
import-linter-example git:(main) ✗ lint-imports
=============
Import Linter
=============
---------
Contracts
---------
Analyzed 7 files, 3 dependencies.
---------------------------------
oil and water are independent KEPT
fire cannot import water (but water can import fire) KEPT
Disallow to import boto3 for security reason KEPT
Only higher layers can import lower ones. stove (high) -> fire (medium) -> oil (low) KEPT
Contracts: 4 kept, 0 broken.
If there is no breach of the contracts, Import Linter is happy. If you break any of the import rules in the codebase, you get detailed errors telling you which import is breaking which contract as follows. Here, I broke all of the rules on purpose.
import-linter-example git:(main) ✗ lint-imports
=============
Import Linter
=============
---------
Contracts
---------
Analyzed 8 files, 7 dependencies.
---------------------------------
oil and water are independent BROKEN
fire cannot import water (but water can import fire) BROKEN
Disallow to import boto3 for security reason BROKEN
Only higher layers can import lower ones: stove (high) -> fire (medium) -> oil (low) BROKEN
Contracts: 0 kept, 4 broken.
----------------
Broken contracts
----------------
oil and water are independent
-----------------------------
project.oil is not allowed to import project.water:
- project.oil -> project.water (l.2)
fire cannot import water (but water can import fire)
----------------------------------------------------
project.fire is not allowed to import project.water:
- project.fire -> project.water (l.1)
Disallow to import boto3 for security reason
--------------------------------------------
project is not allowed to import boto3:
- project.oil -> boto3 (l.1)
Only higher layers can import lower ones: stove (high) -> fire (medium) -> oil (low)
------------------------------------------------------------------------------------
project.oil is not allowed to import project.fire:
- project.oil -> project.fire (l.3)
This way, you can organise your module imports in a more systematic way, and automate this check by including Import Linter as part of the CI process, for example.
You can find the example code from this post in https://github.com/921kiyo/import-linter-example.
P.S: I discovered Import Linter through the Kedro open source project, for which I was a maintainer. You can find the examples of Import Linter in pyproject.toml.