Enforce import rules using the Python import linter

6 minute read

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.

Photo by NeONBRAND Unsplash
Photo by Kishore Kumar

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.

  1. Pip install import-linter.
    pip install import-linter
    
  2. In the project root, create the .importlinter file, where you will specify import rules called contracts.

  3. 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.

Subscribe to my blog

* indicates required
Thank you for reading my blog post! If you liked it, please subscribe to receive new posts. I won't give your address to anyone else, won't send you any spam, and you can unsubscribe at any time.