A dependency linter for Python projects
Project description
python-dependency-linter
A dependency linter for Python projects. Define rules for which modules can depend on what, and catch violations.
What It Does
- Define dependency rules between modules using a simple YAML or TOML config
- Detect imports that violate your rules with a single CLI command
- Integrate into CI or pre-commit to keep your architecture consistent
For Python developers who care about module boundaries and dependency direction — whether you're applying Layered, Hexagonal, Clean Architecture, or your own conventions.
Installation
pip install python-dependency-linter
Or with uv:
uv add python-dependency-linter
Quick Start
Create .python-dependency-linter.yaml in your project root:
rules:
- name: domain-isolation
modules: contexts.*.domain
allow:
standard_library: [dataclasses, typing]
third_party: [pydantic]
local: [contexts.*.domain]
- name: application-dependency
modules: contexts.*.application
allow:
standard_library: ["*"]
third_party: [pydantic]
local:
- contexts.*.application
- contexts.*.domain
Run:
pdl check
Output:
contexts/boards/domain/models.py:6
[domain-isolation] contexts.boards.domain.models → contexts.boards.application.service (local)
contexts/boards/domain/models.py:9
[domain-isolation] contexts.boards.domain.models → sqlalchemy (third_party)
Found 2 violation(s).
Examples
Layered Architecture
Enforce dependency direction: presentation → application → domain, where domain has no outward dependencies.
rules:
- name: domain-isolation
modules: my_app.domain
allow:
standard_library: ["*"]
third_party: []
local: [my_app.domain]
- name: application-layer
modules: my_app.application
allow:
standard_library: ["*"]
third_party: [pydantic]
local:
- my_app.application
- my_app.domain
- name: presentation-layer
modules: my_app.presentation
allow:
standard_library: ["*"]
third_party: [fastapi, pydantic]
local:
- my_app.presentation
- my_app.application
- my_app.domain
Hexagonal Architecture
Isolate domain from infrastructure. Ports (interfaces) live in domain, adapters depend on domain but not vice versa.
rules:
- name: domain-no-infra
modules: contexts.*.domain
allow:
standard_library: [dataclasses, typing, abc]
third_party: []
local: [contexts.*.domain]
- name: adapters-depend-on-domain
modules: contexts.*.adapters
allow:
standard_library: ["*"]
third_party: ["*"]
local:
- contexts.*.adapters
- contexts.*.domain
Configuration
Include / Exclude
Control which files are scanned using include and exclude:
include:
- src
exclude:
- src/generated/**
rules:
- name: ...
- No
includeorexclude— All.pyfiles under the project root are scanned includeonly — Only files matching the given paths are scannedexcludeonly — All files except those matching the given paths are scanned- Both —
includeis applied first, thenexcludefilters within that result
Bare directory names (e.g., src) and trailing-slash forms (e.g., src/) are treated the same as src/**.
In pyproject.toml:
[tool.python-dependency-linter]
include = ["src"]
exclude = ["src/generated/**"]
Rule Structure
Each rule has:
name— Rule identifier, shown in violation outputmodules— Module pattern to apply the rule to (supports*wildcard)allow— Whitelist: only listed dependencies are alloweddeny— Blacklist: listed dependencies are denied
rules:
- name: rule-name
modules: my_package.*.domain
allow:
standard_library: [dataclasses]
third_party: [pydantic]
local: [my_package.*.domain]
deny:
third_party: [boto3]
Import Categories
Dependencies are classified into three categories (per PEP 8):
standard_library— Python built-in modules (os,sys,typing, ...)third_party— Installed packages (pydantic,sqlalchemy, ...)local— Modules in your project
Both absolute imports (from contexts.boards.domain import models) and relative imports (from ..domain import models) are analyzed. Relative imports are resolved to absolute module names based on the file's location.
Behavior
- No rule — Everything is allowed
allowonly — Whitelist mode. Only listed dependencies are alloweddenyonly — Blacklist mode. Listed dependencies are denied, rest allowedallow+deny— Allow first, then deny removes exceptions- If
allowexists but a category is omitted, that category allows all. For example:
rules:
- name: domain-isolation
modules: contexts.*.domain
allow:
third_party: [pydantic]
local: [contexts.*.domain]
# standard_library is omitted → all standard library imports are allowed
Use "*" to allow all within a category:
allow:
standard_library: ["*"] # allow all standard library imports
Wildcard
* matches a single level in dotted module paths:
modules: contexts.*.domain # matches contexts.boards.domain, contexts.auth.domain, ...
** matches one or more levels in dotted module paths:
modules: contexts.**.domain # matches contexts.boards.domain, contexts.boards.sub.domain, ...
Submodule Matching
When a pattern is used in allow or deny, it also matches submodules of the matched module. For example:
allow:
local: [contexts.*.domain]
This allows imports of contexts.boards.domain as well as its submodules like contexts.boards.domain.models or contexts.boards.domain.entities.metric.
Rule Merging
When multiple rules match a module, they are merged. Specific rules override wildcard rules per field:
rules:
- name: base
modules: contexts.*.domain
allow:
third_party: [pydantic]
- name: boards-extra
modules: contexts.boards.domain
allow:
third_party: [attrs] # merged: [pydantic, attrs]
pyproject.toml
You can also configure in pyproject.toml:
[[tool.python-dependency-linter.rules]]
name = "domain-isolation"
modules = "contexts.*.domain"
[tool.python-dependency-linter.rules.allow]
standard_library = ["dataclasses", "typing"]
third_party = ["pydantic"]
local = ["contexts.*.domain"]
[[tool.python-dependency-linter.rules]]
name = "application-dependency"
modules = "contexts.*.application"
[tool.python-dependency-linter.rules.allow]
standard_library = ["*"]
third_party = ["pydantic"]
local = ["contexts.*.application", "contexts.*.domain"]
[[tool.python-dependency-linter.rules]]
name = "no-boto-in-domain"
modules = "contexts.*.domain"
[tool.python-dependency-linter.rules.deny]
third_party = ["boto3"]
CLI
# Check with auto-discovered config (searches upward from cwd)
pdl check
# Specify config file (project root = config file's parent directory)
pdl check --config path/to/config.yaml
Exit codes:
0— No violations1— Violations found2— Config file not found
If no --config is given, the tool searches upward from the current directory for .python-dependency-linter.yaml or pyproject.toml (with [tool.python-dependency-linter]). The config file's parent directory is used as the project root. If no config file is found, the tool prints an error and exits with code 2:
Error: Config file not found. Create .python-dependency-linter.yaml or configure [tool.python-dependency-linter] in pyproject.toml.
Pre-commit
Add to .pre-commit-config.yaml:
- repo: https://github.com/heumsi/python-dependency-linter
rev: v0.1.0
hooks:
- id: python-dependency-linter
To pass custom options (e.g., a different config file):
- repo: https://github.com/heumsi/python-dependency-linter
rev: v0.1.0
hooks:
- id: python-dependency-linter
args: [--config, custom-config.yaml]
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_dependency_linter-0.4.0.tar.gz.
File metadata
- Download URL: python_dependency_linter-0.4.0.tar.gz
- Upload date:
- Size: 35.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ea0c942763f18784ac0170b85b938bf354546524aa7ddf3ed1eb8f69e23d30a
|
|
| MD5 |
c292a7cb4915703aa5fb2aa26024b495
|
|
| BLAKE2b-256 |
e55966e788e1fc8eb674052fbc5fc07983d8be2b9fab84df8db62645484230cb
|
Provenance
The following attestation bundles were made for python_dependency_linter-0.4.0.tar.gz:
Publisher:
publish.yaml on heumsi/python-dependency-linter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_dependency_linter-0.4.0.tar.gz -
Subject digest:
8ea0c942763f18784ac0170b85b938bf354546524aa7ddf3ed1eb8f69e23d30a - Sigstore transparency entry: 1198694316
- Sigstore integration time:
-
Permalink:
heumsi/python-dependency-linter@88e77bdc1ebc30802d30a5b968a88ad6704dcd3e -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/heumsi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@88e77bdc1ebc30802d30a5b968a88ad6704dcd3e -
Trigger Event:
push
-
Statement type:
File details
Details for the file python_dependency_linter-0.4.0-py3-none-any.whl.
File metadata
- Download URL: python_dependency_linter-0.4.0-py3-none-any.whl
- Upload date:
- Size: 11.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06db5123f519b143079ab6149a1717e379432e93a49f95680927ecaca5b2ebca
|
|
| MD5 |
c07ad7594dc21bfcc62c06e175325a92
|
|
| BLAKE2b-256 |
90470af696acd103a77fb8f3d813a8ecccb4d477e17df790f6fde6de55f78ea5
|
Provenance
The following attestation bundles were made for python_dependency_linter-0.4.0-py3-none-any.whl:
Publisher:
publish.yaml on heumsi/python-dependency-linter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_dependency_linter-0.4.0-py3-none-any.whl -
Subject digest:
06db5123f519b143079ab6149a1717e379432e93a49f95680927ecaca5b2ebca - Sigstore transparency entry: 1198694352
- Sigstore integration time:
-
Permalink:
heumsi/python-dependency-linter@88e77bdc1ebc30802d30a5b968a88ad6704dcd3e -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/heumsi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@88e77bdc1ebc30802d30a5b968a88ad6704dcd3e -
Trigger Event:
push
-
Statement type: