import-linter

import-linter Tool Analysis

Overview

import-linter checks that a Python project obeys a set of import contracts — rules about which parts of the codebase may import which other parts. It is the only tool Lintro wraps that reasons about the shape of a package’s import graph rather than about individual source lines.

The command-line entry point is lint-imports; the distribution on PyPI is import-linter.

Core Tool Capabilities

import-linter builds the import graph of a configured root_package (or root_packages) with grimp and evaluates every declared contract against it. The built-in contract types are:

Contract typeWhat it enforces
layersHigher layers may import lower ones, never the reverse
forbiddenNamed source modules must not import named forbidden modules
independenceA set of modules must not import one another in either direction
protectedOnly allowlisted modules may import the protected modules
acyclic_siblingsSibling modules under a parent must not form an import cycle

Contracts live in pyproject.toml ([tool.importlinter]), .importlinter, or setup.cfg ([importlinter]). Native exit codes are:

SituationNative lint-imports
Contract brokenexit 1
Config present, contracts all keptexit 0
Config present, zero contractsexit 0
No config file at allerror (non-zero)

Lintro Implementation Analysis

Preserved features

  • The full contract set is read from the project’s own native configuration. Lintro never synthesises or overrides contracts.
  • Every contract type is supported, because Lintro parses the reported import chains rather than modelling contract semantics itself.

Design decisions

  • Project-scoped, runs once. import-linter analyses a whole package graph, so the plugin ignores the discovered file list and invokes lint-imports a single time per run. File discovery on *.py only decides whether the tool is relevant for the paths given.
  • Config resolution walks upward. The filename order — setup.cfg, .importlinter, then pyproject.toml — is the same order lint-imports uses. What Lintro adds is the search: it walks up from each input path rather than only looking in the process working directory, and runs the tool from the directory of the file it finds so the root package is importable. A candidate counts only when it really carries an import-linter section (the TOML is parsed, not string-matched), so a project’s ordinary pyproject.toml is not mistaken for import-linter configuration.
  • No configuration found is a clean result, not a failure. This is a deliberate divergence: the native tool errors when it can find no config file. Reporting clean lets import-linter stay enabled by default for projects that declare no contracts. This is a different case from a config that exists with zero contracts — there the native tool already exits 0 and Lintro simply passes that through.
  • --no-cache is always passed so a check never writes a cache directory into the project being linted, and --no-logo keeps the ASCII banner out of parsed output.

Issue mapping

import-linter reports architectural violations, not line-level defects, so the Lintro issue fields are mapped accordingly:

Lintro fieldValue
fileDotted path of the importing module that starts the chain
lineAlways 0 — a chain is not anchored to one line
codeName of the broken contract
messageThe import chain, e.g. pkg.low -> pkg.a -> pkg.high
severityERROR — a broken contract fails the run

One issue is emitted per broken chain, so a contract broken by three distinct chains produces three issues.

Limitations

  • No fix mode. Contract violations are design problems; there is nothing safe to rewrite automatically. lintro format never invokes import-linter.
  • Requires an importable root package. grimp imports the package to build the graph, so the project’s dependencies must be installed in the environment Lintro runs in.
  • file is a module path, not a file path. A contract is broken by an import relationship, not by a source line, so the file field carries a dotted module name and line is 0. Consumers that expect a filesystem path — SARIF artifact URIs, editor links, AI fix-path resolution — will not resolve these issues to a source file.

Usage Comparison

NativeLintro
lint-importslintro check .
lint-imports --config setup.cfglintro check . (config auto-discovered)
n/alintro check . --tools import-linter

Configuration Strategy

import-linter is fully native-config driven — see configuration.md for the contract syntax. Lintro injects nothing into the contract set.

Recommendations

  • Start with a single layers contract over the top-level packages and add contracts as the boundaries stabilise.
  • Keep contracts in pyproject.toml alongside the rest of the tool configuration so there is one place to look — but note the precedence above: if the project already has a setup.cfg or .importlinter carrying an [importlinter] section, that file wins and contracts added to pyproject.toml are never read. Move the whole configuration rather than splitting it.