Cycle and internal-dependency rules
ArchUnitPython
Architecture testing for Python. Turn imports and source structure into rules that run beside pytest or unittest. ArchUnitPython checks boundaries, cycles, layers, external dependencies, code metrics, and architecture diagrams without becoming a production dependency.
PyPI
pip install archunitpythonfrom archunitpython import project_files, assert_passes
def test_domain_stays_independent():
rule = (
project_files('src/')
.in_folder('**/domain/**')
.should_not()
.depend_on_files()
.in_folder('**/infrastructure/**')
)
assert_passes(rule)Silent product demo
Watch architecture rules run in Python.
This silent walkthrough follows an ArchUnitPython rule from its test definition to the resulting architecture feedback. Playback starts muted, loops in place, and keeps standard controls available so you can inspect each step.
Watch on YouTube ↗The ecosystem
Python, in context.
Python is a readable, dynamically typed language used across web services, automation, data engineering, machine learning, and scientific computing. Frameworks such as Django, FastAPI, and Flask make it quick to grow an application, while architecture tests help that speed stay compatible with clear module ownership.
Web applications, data platforms, automation, and AI
Django, FastAPI, Flask, pytest, and unittest
Dynamic imports analyzed without a runtime dependency
Adoption snapshot
Public adoption, in context.
PyPI Stats reports 191,664 downloads in its retained public history, excluding mirrors, with 45,927 in the latest completed 30-day window.
- Package status
- Published on PyPI Public download totals
- Reported history
- 191,664 Apr 6 to Oct 3, 2026
- Latest 30 days
- 45,927 Sep 4 to Oct 3, 2026
- GitHub stars
- 667 Snapshot of October 4, 2026
Capabilities
What ArchUnitPython brings to the test suite.
The family shares a recognizable architecture vocabulary while each implementation stays honest about its language, toolchain, and maturity.
Named layers and forbidden external imports
Counting, cohesion, and distance metrics
pytest, unittest, and framework-neutral checks
Ignore files and JSON-configured common rules
Dependency graph snapshots and exports
Go deeper
Protect the direction between named layers.
Named layers describe the intended flow once, then validate all matching imports together. This is useful for services that grew from a small script into routes, domain logic, and persistence adapters.
from archunitpython import layers, assert_passes
def test_layer_direction():
rule = (
layers()
.layer('API').defined_by('app/api/**')
.layer('Domain').defined_by('app/domain/**')
.layer('Data').defined_by('app/data/**')
.where_layer('Domain')
.may_not_depend_on_layers("API")
)
assert_passes(rule)Documentation highlights
Work with ArchUnitPython beyond the quickstart.
ArchUnitPython analyzes source without importing the target application. It turns ordinary Python modules into a dependency graph, preserves useful import context, and lets pytest, unittest, or any caller assert on structured violations.
Analyze Python without executing it
The extractor walks .py files, parses them with Python ASTs, and resolves absolute and relative imports back to project files or external module names.
- .archignore augments safe defaults for virtual environments, caches, builds, generated code, and project-specific exclusions.
- Literal importlib.import_module() and __import__() calls are recognized, while non-literal dynamic behavior is not guessed.
- TYPE_CHECKING and conditional ImportError paths retain their import kind, and scoped ignore comments can remove known compatibility edges.
Express files, layers, and slices
File rules check naming, placement, cycles, internal dependencies, external dependencies, or a custom predicate. Layer and slice rules project the same graph into larger architectural units.
- Named layers support allowlists and blocklists, including sealed layers that may not depend on another named layer.
- Slices derive names from paths or regular expressions and can enforce an allowed component diagram.
- Selectors fail when they match nothing by default, preventing a renamed folder from turning a useful test into a silent pass.
Set thresholds from evidence
Metric comparisons are exact and evaluate each selected file or class independently. The practical starting point is the current project baseline, tightened deliberately over time.
- Use strict or inclusive comparisons explicitly, such as should_be_below() versus should_be_below_or_equal().
- Count, LCOM, coupling, instability, abstractness, and main-sequence distance cover both local size and graph-level design pressure.
- Graph and metric results export to reviewable formats, while because() keeps the architectural reason beside the threshold.
Release and compatibility
Published package
Published on PyPI, designed with zero runtime dependencies, and compatible with ordinary Python testing workflows.
Where it fits
Put boundaries next to the code they protect.
Common starting points for ArchUnitPython include the following project shapes.
More ecosystems
The same intent, expressed natively.
Explore sibling implementations and carry the architecture vocabulary across a polyglot system.
Make the first boundary executable.
Add one meaningful rule, run it with the existing tests, and grow the architecture suite as the system reveals where it needs protection.
Start with ArchUnitPython ↗

