pymodularity
The underscore's position in a Python import path sets the import's scope.
model: Rust's pub(super) and pub(crate), applied to PEP 8 underscores.
pymodularity does 3 jobs on a repo's real Python imports:
- it draws them as mermaid diagrams.
- it checks them against conventions derived from the code itself, with no allow-list to maintain.
- it reports which files and directories carry the most structural coupling.
The analysis needs only the standard library, so pymodularity has no runtime dependencies.
exception: check's dependency rule needs uv on PATH.
Install and run
uvx pymodularity check .
uvx pymodularity graph . --out docs/imports
uvx pymodularity metrics .
Or, inside a project: uv add --dev pymodularity and run pymodularity ... in that project's venv.
The rules check enforces
pymodularity check . exits 1 on any finding.
Each finding names the importing file, the target and the owner.
- No import cycles: between files, and between directories at every depth.
TYPE_CHECKINGimports and in-function imports count.- no baseline: an existing cycle fails too.
- Private items stay inside their owner: only code inside the owner may import a
_-prefixed module or name.- private module: the owner is the directory holding it.
- private name from a package: the owner is the package directory.
- private name from a regular file: the owner is that file alone, so even a sibling file is a finding.
- attribute access such as
mod._xcounts like an import.
- Declared dependencies match the imports: deptry reports unused, missing and transitive dependencies per package root.
- requirement:
uvon PATH. Without it,checkexits 2. --no-depsskips this rule.
- requirement:
- Structure limits: at most 500 lines per file, 10 entries per directory, 100 files per package root and 10 public names per file.
- no config, no baseline.
- ceiling: a limit marks the size that is clearly too much. It is no target.
- below a limit: a smaller interface is still better.
metricsreports interface width and exposure share.
Worked cases: the showcase shows each rule except the dependency rule as allowed and finding cases. Definitions: docs/rules.md holds the counting rules, the deptry run, and the ruff rules for functions and classes.
The 3 subcommands
All 3 also take a repeatable --exclude PATTERN, merged with [tool.pymodularity] exclude in <repo>'s own pyproject.toml.
Pattern: fnmatch against the POSIX path relative to <repo>, matched on the path itself or any parent directory.
Use case: generated code (an OpenAPI client, protobuf stubs) and vendored code.
Effect: an excluded tree is invisible to every rule and report, except deptry's scan of a kept package that contains it.
An import from kept code into it behaves like an import of code pymodularity can't resolve.
Details: docs/rules.md.
pymodularity check <repo> [--no-deps]: runs the 4 rules above and prints the findings.- exit code: 1 on any finding.
--no-deps: skips the dependency rule.
pymodularity graph <repo> --out DIR [--html]: writes one mermaid view per directory underDIR.index.md: written for every view.--html: adds a clickableindex.htmlper view. The browser needs network access to load mermaid from a CDN.
pymodularity metrics <repo>: prints a coupling report to stdout.- reach: transitive dependents and dependencies, plus a
scoreper file and per directory. - interface width: the names other code imports from a unit.
- exposure share: how much of what a file defines other code uses.
- entry files: how much of a directory outside code reaches into.
- misplaced files: files whose closest partners sit mostly in another directory.
- exit code: always 0, because the report is no gate.
- reach: transitive dependents and dependencies, plus a
Blind spots
Worked cases: the showcase gap cases G1 to G8, each tagged with its outcome.
pymodularity shows import coupling only.
Import coupling is a lower bound on logical coupling: "if A changes, must B change?"
Coupling that bypasses Python imports stays invisible:
- HTTP APIs, shared database schemas and tables, config keys and env vars, message and file formats, string-keyed registries, runtime
sys.pathedits. - Two files with no edge can still force each other's edits. Never read a missing edge as independence.
Other limits:
- ambiguous import:
checkprintswarning: ambiguous importto stderr and draws no edge.- cause: several import roots provide the module, and none is an ancestor of the importer.
- runtime
sys.pathedit: an import that resolves only through such an edit gets an edge only if the inserted directory is an import root. - script sibling:
python main.pyimporting its siblingexecution.py, with no package root nearby, gets no edge.- reason: resolution looks only through import roots, never through the importer's own directory.
- tests:
pymodularityalways skips test directories (tests/,test/) and test files (test_*.py,*_test.py,conftest.py).- reason: a test importing a sibling would draw a production dependency that production never has.
metricsas a gate: its numbers move even on a repo that passescheck.- consequence: a gate on them rewards gaming the number, for example replacing an import with a string-keyed registry.
- attribute form, missed cases:
- an instance attribute such as
obj._y, becausepymodularityhas no type information. - complement for that case: ruff's
SLF001. It has no owner scope, so it also flags access from inside the owner. - a base name bound to a third-party module, or to a module outside the repo.
getattr(mod, "_x")and otherimportlibaccess.- a name the file also rebinds anywhere: a parameter, an assignment target, a
for,with,exceptor comprehension target, a nesteddeforclass. - reason for the last case: a gate must trade false findings for missed ones, so
pymodularityskips such a name.
- an instance attribute such as
Metadata
Release files for pymodularity 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pymodularity-0.3.0.tar.gz | 39.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pymodularity-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 86.2 kB
Release files / pymodularity-0.3.0.tar.gz
| Download URL | pymodularity-0.3.0.tar.gz |
|---|---|
| Size | 39.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
be1b42e2d879c3f8c0fb7f260e53354d77689a5ae9a8a45a6049e780813a32dd
|
|
BLAKE2b-256 checksum How to use checksums |
cb3b337282f3565b83dc021acc4fd6f6a9b6a349aaeeb08d962fc1c66f983adb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / pymodularity-0.3.0-py3-none-any.whl
| Download URL | pymodularity-0.3.0-py3-none-any.whl |
|---|---|
| Size | 47.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c24f13bb9b59bde72a83ce69564418b819e7f5b7ca8c79a5c15bff2d32bf9a49
|
|
BLAKE2b-256 checksum How to use checksums |
6efa8515bbf6562c9438d7043e9fbd65c04c7e574e8756d0373bc50261721380
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for pymodularity 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pymodularity-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.