Package dependency metrics for Python — Abstractness, Instability, Distance from Main Sequence
Project description
PyMainSequence
Package dependency metrics for Python — Abstractness, Instability, and Distance from the Main Sequence, as described by Robert C. Martin (Uncle Bob) in Clean Architecture.
Think JDepend or NETDepend, but for Python.
Metrics
| Metric | Symbol | Formula | Range | Meaning |
|---|---|---|---|---|
| Afferent Coupling | Ca | — | ≥ 0 | Components that depend on this one (fan-in) |
| Efferent Coupling | Ce | — | ≥ 0 | Components this one depends on (fan-out) |
| Instability | I | Ce / (Ca + Ce) |
0–1 | 0 = maximally stable, 1 = maximally unstable |
| Abstractness | A | abstract / total classes |
0–1 | 0 = fully concrete, 1 = fully abstract |
| Distance | D | |A + I - 1| |
0–1 | Distance from the Main Sequence; 0 = on the line |
The Main Sequence is the line A + I = 1. Components on it balance stability with abstraction.
- Zone of Pain (I≈0, A≈0): stable but fully concrete — hard to change, painful to extend.
- Zone of Uselessness (I≈1, A≈1): unstable and fully abstract — nobody depends on it.
Abstract classes, abc.ABC subclasses, @abstractmethod methods, and typing.Protocol subclasses all count toward abstractness.
Installation
pip install pymainsequence
# or with uv:
uv add pymainsequence
Requires Python 3.12+.
CLI
pymainsequence analyze
Print metrics for every component (package) found under a path:
$ pymainsequence analyze ./src
╭───────────┬────┬────┬──────┬──────┬──────┬─────────╮
│ Component │ Ca │ Ce │ I │ A │ D │ Classes │
├───────────┼────┼────┼──────┼──────┼──────┼─────────┤
│ api │ 0 │ 2 │ 1.00 │ 0.00 │ 0.00 │ 1 │
│ core │ 1 │ 1 │ 0.50 │ 0.50 │ 0.00 │ 2 │
│ utils │ 2 │ 0 │ 0.00 │ 0.00 │ 1.00 │ 1 │
╰───────────┴────┴────┴──────┴──────┴──────╯─────────╯
3 component(s)
Distance is colour-coded: green (D ≤ 0.3), yellow (D ≤ 0.5), red (D > 0.5).
Options:
| Flag | Default | Description |
|---|---|---|
--depth N |
1 |
Package nesting depth treated as components |
--include-external |
off | Count stdlib/third-party imports in Ce |
--format table|json |
table |
Output format |
pymainsequence analyze ./src --depth 2 --format json
pymainsequence check
Assert constraints and exit non-zero on violations — designed for CI:
pymainsequence check ./src --max-distance 0.5 --max-instability 0.8
# exit 0 on pass, exit 1 on violation
Options:
| Flag | Description |
|---|---|
--max-distance F |
Maximum allowed D per component |
--max-instability F |
Maximum allowed I per component |
--max-ce N |
Maximum allowed efferent coupling |
--no-dep FROM TO |
Forbidden dependency (repeatable) |
pymainsequence check ./src --no-dep core api --no-dep core infra
Programmatic API
from pymainsequence import analyze
report = analyze("./src")
for component in report:
print(f"{component.name}: I={component.instability:.2f} A={component.abstractness:.2f} D={component.distance:.2f}")
core = report.get("core")
print(core.ca, core.ce, core.afferent, core.efferent)
analyze() accepts an optional depth (default 1) and include_external (default False).
Constraint API
Use enforce() to write architectural tests with pytest (or any test runner):
from pymainsequence import analyze
from pymainsequence.rules import enforce
def test_architecture():
report = analyze("./src")
enforce(report) \
.max_instability(0.8) \
.max_distance(0.5) \
.no_dependency("core", "api") \
.check()
Or as a context manager:
def test_architecture():
report = analyze("./src")
with enforce(report) as e:
e.max_instability(0.8)
e.max_distance(0.5)
e.no_dependency("core", "api")
# raises ViolationError with a full list of offenders on exit
Available constraints
| Method | Description |
|---|---|
.max_instability(threshold) |
I ≤ threshold for all (or one) component |
.max_distance(threshold) |
D ≤ threshold |
.max_abstractness(threshold) |
A ≤ threshold |
.max_ce(n) |
Efferent coupling ≤ n |
.max_ca(n) |
Afferent coupling ≤ n |
.stable_abstractions_principle(threshold=0.5) |
Alias for max_distance |
.no_dependency(from, to) |
No direct dependency from → to |
.check() |
Raise ViolationError listing all violations |
All methods accept an optional component="name" keyword to scope the check to a single component.
How components are determined
A component is a Python package (directory with __init__.py) or a top-level .py file directly under the analyzed path. At --depth 1 (default), only immediate sub-packages are components; files nested deeper still belong to their parent component.
Only imports between components within the analyzed path are counted. Standard library and third-party imports are ignored by default (--include-external to change this).
Relative imports (from . import x, from ..utils import y) are resolved to their absolute module paths before matching.
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 pymainsequence-0.1.0.tar.gz.
File metadata
- Download URL: pymainsequence-0.1.0.tar.gz
- Upload date:
- Size: 23.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69f79f47425ebb5b7daa918acea346208088c7b53ac60072828a1b001d9325c0
|
|
| MD5 |
d5ff580877520908089ee31372387bca
|
|
| BLAKE2b-256 |
f23c8acb701634164523241b71154f3dcd1420dff944ea8ae03cd9ab0e2543ee
|
File details
Details for the file pymainsequence-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pymainsequence-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.17 {"installer":{"name":"uv","version":"0.9.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
977b0ca5958f7358bed3c9b18f091f7eea9535abb8f87e4ddfa9c4f36f0ba7cf
|
|
| MD5 |
326151e344cc43c06cb4b2731533dfde
|
|
| BLAKE2b-256 |
db468e2461aec0c4dd29b9a4e0102d0b0ccabe1c2d3bc0c61551f023a2e451f8
|