Skip to content

Repository files navigation

ArchUnitPython - Architecture Testing

ArchUnitPython Logo

License: MITBuild & testsGitHub stars
PyPI downloadsPyPI total downloads

Enforce architecture rules in Python projects. Check for dependency directions, detect circular dependencies, enforce coding standards and much more. Integrates with pytest and any other testing framework. Very simple setup and pipeline integration. Zero runtime dependencies.

The #1 ArchUnit-style architecture testing library for Python, measured by GitHub stars.

Inspired by the amazing ArchUnit library but we are not affiliated with ArchUnit.

SetupUse CasesFeaturesWhy ArchUnitPython?SponsorContributing

⚡ 5 min Quickstart

Installation

pip install archunitpython

Add tests

Simply add tests to your existing test suites. The following is an example using pytest. First we ensure that we have no circular dependencies.

fromarchunitpythonimportproject_files, metrics, assert_passesdeftest_no_circular_dependencies():
rule=project_files("src/").in_folder("src/**").should().have_no_cycles()
assert_passes(rule)

Next we ensure that our layered architecture is respected.

deftest_presentation_should_not_depend_on_database():
rule= (
project_files("src/")
.in_folder("**/presentation/**")
.should_not()
.depend_on_files()
.in_folder("**/database/**")
)
assert_passes(rule)
deftest_business_should_not_depend_on_database():
rule= (
project_files("src/")
.in_folder("**/business/**")
.should_not()
.depend_on_files()
.in_folder("**/database/**")
)
assert_passes(rule)
# More layers ...

Lastly we ensure that some code metric rules are met.

deftest_no_large_files():
rule=metrics("src/").count().lines_of_code().should_be_below(1000)
assert_passes(rule)
deftest_high_cohesion():
# LCOM metric (lack of cohesion of methods), low = high cohesionrule=metrics("src/").lcom().lcom96b().should_be_below(0.3)
assert_passes(rule)

CI Integration

These tests run automatically in your testing setup, for example in your CI pipeline, so that's basically it. This setup ensures that the architectural rules you have defined are always adhered to!

# GitHub Actions
- name: Run Architecture Testsrun: pytest tests/test_architecture.py -v

You can also export dependency graph reports as CI artifacts:

fromarchunitpythonimportproject_graphdeftest_generate_dependency_graph_reports():
graph=project_graph("src/").titled("Application Architecture")
graph.collapse_to_folder_depth(2).export_as_html(
"reports/dependency-graph.html"
)
graph.export_as_mermaid("reports/dependency-graph.mmd")
assertgraph.snapshot().summary.node_count>=0

🚐 Setup

Installation:

pip install archunitpython

That's it. Works with pytest, unittest, or any Python testing framework.

pytest (Recommended)

Use assert_passes() for clean assertion messages:

fromarchunitpythonimportproject_files, assert_passesdeftest_my_architecture():
rule=project_files("src/").should().have_no_cycles()
assert_passes(rule)

Any Other Framework

Use .check() directly and assert on the violations list:

fromarchunitpythonimportproject_filesrule=project_files("src/").should().have_no_cycles()
violations=rule.check()
assertlen(violations) ==0

Configuration Options

Both assert_passes() and .check() accept configuration options:

fromarchunitpythonimportCheckOptionsoptions=CheckOptions(
allow_empty_tests=True, # Don't fail when no files matchclear_cache=True, # Clear the graph cacheignore_type_checking_imports=True, # Ignore imports inside if TYPE_CHECKING
)
violations=rule.check(options)

Explaining Rules With .because(...)

Attach a rationale to a rule so failing assertions explain why the rule exists:

rule= (
project_files("src/")
.in_folder("**/controllers/**")
.should_not()
.depend_on_files()
.in_folder("**/database/**")
.because("controllers should stay thin and delegate persistence")
)
assert_passes(rule)

When the rule fails, the rationale is included in the assertion message.

🐹 Use Cases

Here is an overview of common use cases.

Layered Architecture:

Enforce that higher layers don't depend on lower layers and vice versa.

Clean Architecture / Hexagonal:

Validate that domain logic doesn't depend on infrastructure.

Microservices / Modular:

Ensure services/modules don't have forbidden cross-dependencies.

🐲 Example Repository

Here is a repository with a fully functioning example that uses ArchUnitPython to ensure architectural rules:

🐣 Features

This is an overview of what you can do with ArchUnitPython.

Circular Dependencies

deftest_services_cycle_free():
rule=project_files("src/").in_folder("**/services/**").should().have_no_cycles()
assert_passes(rule)

Layer Dependencies

deftest_clean_architecture_layers():
rule= (
project_files("src/")
.in_folder("**/presentation/**")
.should_not()
.depend_on_files()
.in_folder("**/database/**")
)
assert_passes(rule)
deftest_business_not_depend_on_presentation():
rule= (
project_files("src/")
.in_folder("**/business/**")
.should_not()
.depend_on_files()
.in_folder("**/presentation/**")
)
assert_passes(rule)

Named Layer Rules

fromarchunitpythonimportproject_layersdeftest_clean_architecture_layers():
rule= (
project_layers("src/")
.layer("presentation").defined_by_folder("**/presentation/**")
.layer("business").defined_by_folder("**/business/**")
.layer("database").defined_by_folder("**/database/**")
.where_layer("presentation")
.may_only_depend_on_layers("business")
.where_layer("business")
.may_only_depend_on_layers()
.where_layer("database")
.may_only_depend_on_layers()
)
assert_passes(rule)

External Dependencies

deftest_domain_does_not_import_requests():
rule= (
project_files("src/")
.in_folder("**/domain/**")
.should_not()
.depend_on_external_modules()
.matching("requests")
)
assert_passes(rule)

TYPE_CHECKING-aware Analysis

fromarchunitpythonimportCheckOptionsdeftest_type_only_dependencies_do_not_count_as_runtime_coupling():
rule= (
project_files("src/")
.in_folder("**/api/**")
.should_not()
.depend_on_files()
.in_folder("**/infrastructure/**")
)
assert_passes(rule, CheckOptions(ignore_type_checking_imports=True))

Dynamic Imports and Ignore Directives

ArchUnitPython detects string-based dynamic imports such as importlib.import_module("my_app.adapters.sql") and __import__("my_app.adapters.sql"). For known migration shims, you can suppress one import edge locally:

frommy_app.adapters.sqlimportRepository# archunit: ignore

Naming Conventions

deftest_naming_patterns():
rule= (
project_files("src/")
.in_folder("**/services/**")
.should()
.have_name("*_service.py")
)
assert_passes(rule)

Code Metrics

deftest_no_large_files():
rule=metrics("src/").count().lines_of_code().should_be_below(1000)
assert_passes(rule)
deftest_high_class_cohesion():
rule=metrics("src/").lcom().lcom96b().should_be_below(0.3)
assert_passes(rule)
deftest_method_count():
rule=metrics("src/").count().method_count().should_be_below(20)
assert_passes(rule)
deftest_field_count_for_data_classes():
rule= (
metrics("src/")
.for_classes_matching("*Data*")
.count()
.field_count()
.should_be(3)
)
assert_passes(rule)

Distance Metrics

deftest_proper_coupling():
rule=metrics("src/").distance().distance_from_main_sequence().should_be_below(0.3)
assert_passes(rule)
deftest_not_in_zone_of_pain():
rule=metrics("src/").distance().not_in_zone_of_pain()
assert_passes(rule)

Custom Rules

You can define your own custom rules.

rule_desc="Python files should have docstrings"defhas_docstring(file):
return'"""'infile.contentor"'''"infile.contentviolations= (
project_files("src/")
.with_name("*.py")
.should()
.adhere_to(has_docstring, rule_desc)
.check()
)
assertlen(violations) ==0

Custom Metrics

You can define your own metrics as well.

deftest_method_field_ratio():
rule= (
metrics("src/")
.custom_metric(
"methodFieldRatio",
"Ratio of methods to fields",
lambdaci: len(ci.methods) /max(len(ci.fields), 1),
)
.should_be_below(10)
)
assert_passes(rule)

Architecture Slices

importrefromarchunitpythonimportproject_slicesdeftest_adhere_to_diagram():
diagram="""@startuml component [controllers] component [services] [controllers] --> [services]@enduml"""rule= (
project_slices("src/")
.defined_by_regex(re.compile(r"/([^/]+)/[^/]+\.py$"))
.should()
.adhere_to_diagram(diagram)
)
assert_passes(rule)
deftest_no_forbidden_dependency():
rule= (
project_slices("src/")
.defined_by("src/(**)/**")
.should_not()
.contain_dependency("services", "controllers")
)
assert_passes(rule)

Dependency Graph Reports

Generate dependency graph reports in multiple formats and narrow them to the part of the codebase you want to inspect.

Using requests library repo for example

fromarchunitpythonimportproject_graphdeftest_export_dependency_graph_reports():
graph=project_graph("src/requests").titled("Application Architecture")
graph.collapse_to_folder_depth(2).export_as_mermaid("reports/dependencies.md")
if__name__=="__main__":
test_export_dependency_graph_reports()

Exported mermaid diagram

flowchart LR
n0["__init__.py"]
n1["__version__.py"]
n2["_internal_utils.py"]
n3["_types.py"]
n4["adapters.py"]
n5["api.py"]
n6["auth.py"]
n7["certs.py"]
n8["compat.py"]
n9["cookies.py"]
n10["exceptions.py"]
n11["help.py"]
n12["hooks.py"]
n13["models.py"]
n14["packages.py"]
n15["sessions.py"]
n16["status_codes.py"]
n17["structures.py"]
n18["utils.py"]
n0 --> n1
n0 --> n5
n0 --> n10
n0 --> n13
n0 --> n15
n0 --> n16
n2 --> n8
n3 --> n6
n3 --> n9
n3 --> n13
n3 --> n17
n4 --> n0
n4 --> n3
n4 --> n6
n4 --> n8
n4 --> n9
n4 --> n10
n4 --> n13
n4 --> n17
n4 --> n18
n5 --> n0
n5 --> n13
n6 --> n2
n6 --> n8
n6 --> n9
n6 --> n13
n6 --> n18
n9 --> n2
n9 --> n3
n9 --> n8
n9 --> n13
n10 --> n8
n10 --> n13
n11 --> n0
n12 --> n0
n12 --> n13
n13 --> n0
n13 --> n2
n13 --> n4
n13 --> n6
n13 --> n8
n13 --> n9
n13 --> n10
n13 --> n12
n13 --> n16
n13 --> n17
n13 --> n18
n14 --> n8
n15 --> n0
n15 --> n2
n15 --> n3
n15 --> n4
n15 --> n6
n15 --> n8
n15 --> n9
n15 --> n10
n15 --> n12
n15 --> n13
n15 --> n16
n15 --> n17
n15 --> n18
n16 --> n17
n17 --> n8
n18 --> n0
n18 --> n1
n18 --> n2
n18 --> n3
n18 --> n8
n18 --> n9
n18 --> n10
n18 --> n13
n18 --> n17
Loading

Supported formats:

  • DOT (export_as_dot, to_dot)
  • Mermaid (export_as_mermaid, to_mermaid)
  • D2 (export_as_d2, to_d2)
  • CSV (export_as_csv, to_csv)
  • JSON (export_as_json, to_json)
  • HTML (export_as_html, to_html)

Graph exploration options:

  • focus_on(pattern, depth) keeps matching files and their neighbors.
  • reachable_from(pattern) keeps matching files and their transitive dependencies.
  • dependents_of(pattern) keeps files that transitively depend on the matching files.
  • collapse_to_folder_depth(depth) aggregates files to folder-level graph nodes.
  • collapse_by_pattern(pattern, replacement) maps files to custom graph nodes.
  • include_external_dependencies() includes imports to external modules such as requests or sqlalchemy.
  • include_self_dependencies() keeps self edges that are normally hidden in reports.

When you create reports through project_graph("src/"), internal file paths are displayed relative to that project root so the output stays readable.

Reports

Generate HTML reports for your metrics. Note that this feature is in beta.

fromarchunitpython.metrics.fluentapi.export_utilsimportMetricsExporter, ExportOptionsMetricsExporter.export_as_html(
{"MethodCount": 5, "FieldCount": 3, "LinesOfCode": 150},
ExportOptions(
output_path="reports/metrics.html",
title="Architecture Metrics Dashboard",
),
)

🔎 Pattern Matching System

We offer three targeting options for pattern matching across all modules:

  • with_name(pattern) - Pattern is checked against the filename (e.g. service.py from src/services/service.py)
  • in_path(pattern) - Pattern is checked against the full relative path (e.g. src/services/service.py)
  • in_folder(pattern) - Pattern is checked against the path without filename (e.g. src/services from src/services/service.py)

For the metrics module there is an additional one:

  • for_classes_matching(pattern) - Pattern is checked against class names. The filepath or filename does not matter here

Pattern Types

We support string patterns and regular expressions. String patterns support glob.

# String patterns with glob support (case sensitive)
.with_name("*_service.py") # All files ending with _service.py
.in_folder("**/services") # All files in any services folder
.in_path("src/api/**/*.py") # All Python files under src/api# Regular expressionsimportre
.with_name(re.compile(r".*Service\.py$"))
.in_folder(re.compile(r"services$"))
# For metrics module: Class name matching
.for_classes_matching("*Service*")
.for_classes_matching(re.compile(r"^User.*"))

Glob Patterns Guide

Basic Wildcards

  • * - Matches any characters within a single path segment (except /)
  • ** - Matches any characters across multiple path segments
  • ? - Matches exactly one character

Common Glob Examples

# Filename patterns
.with_name("*.py") # All Python files
.with_name("*_service.py") # Files ending with _service.py
.with_name("test_*.py") # Files starting with test_# Folder patterns
.in_folder("**/services") # Any services folder at any depth
.in_folder("src/services") # Exact src/services folder
.in_folder("**/test/**") # Any folder containing test in path# Path patterns
.in_path("src/**/*.py") # Python files anywhere under src
.in_path("**/test/**/*_test.py") # Test files in any test folder

Recommendation

We generally recommend using string patterns with glob support unless you need very special cases. Regular expressions add extra complexity that is not necessary for most cases.

Supported Metric Types

LCOM (Lack of Cohesion of Methods)

The LCOM metrics measure how well the methods and fields of a class are connected. Lower values indicate better cohesion.

# LCOM96a (Henderson et al.)metrics("src/").lcom().lcom96a().should_be_below(0.8)
# LCOM96b (Henderson et al.) - most commonly usedmetrics("src/").lcom().lcom96b().should_be_below(0.7)

All 8 LCOM variants are available: lcom96a(), lcom96b(), lcom1() through lcom5(), and lcomstar().

The LCOM96b metric is calculated as:

LCOM96b = (1/a) * sum((1/m) * (m - mu(Ai)))

Where:

  • m is the number of methods in the class
  • a is the number of attributes (fields) in the class
  • mu(Ai) is the number of methods that access attribute Ai

The result is a value between 0 and 1:

  • 0: perfect cohesion (all methods access all attributes)
  • 1: complete lack of cohesion (each method accesses its own attribute)

Count Metrics

metrics("src/").count().method_count().should_be_below(20)
metrics("src/").count().field_count().should_be_below(15)
metrics("src/").count().lines_of_code().should_be_below(200)
metrics("src/").count().statements().should_be_below(100)
metrics("src/").count().imports().should_be_below(20)

Distance Metrics

metrics("src/").distance().abstractness().should_be_above(0.3)
metrics("src/").distance().instability().should_be_below(0.8)
metrics("src/").distance().distance_from_main_sequence().should_be_below(0.5)

Custom Metrics

metrics("src/").custom_metric(
"complexityRatio",
"Ratio of methods to fields",
lambdaci: len(ci.methods) /max(len(ci.fields), 1),
).should_be_below(3.0)

📐 UML Diagram Support

ArchUnitPython can validate your architecture against PlantUML diagrams, ensuring your code matches your architectural designs.

Component Diagrams

deftest_component_architecture():
diagram="""@startumlcomponent [UserInterface]component [BusinessLogic]component [DataAccess][UserInterface] --> [BusinessLogic][BusinessLogic] --> [DataAccess]@enduml"""rule= (
project_slices("src/")
.defined_by("src/(**)/**")
.should()
.adhere_to_diagram(diagram)
)
assert_passes(rule)

Diagram from File

deftest_from_file():
rule= (
project_slices("src/")
.defined_by("src/(**)/**")
.should()
.adhere_to_diagram_in_file("docs/architecture.puml")
)
assert_passes(rule)

📊 Library Comparison

Here's how ArchUnitPython compares to other Python architecture-enforcement libraries.

ArchUnitPython is optimized for architecture rules as tests: rules live next to your normal unit tests, run in pytest/unittest/CI, and fail with test-style violation messages. Broader CLI-first tools such as Tach and Import Linter are excellent adjacent tools, but they solve the problem through separate configuration and commands rather than a test-native ArchUnit-style API.

FeatureArchUnitPythonTachImport LinterPyTestArch
Primary workflow✅ Architecture rules as unit tests⚠️ CLI + tach.toml⚠️ CLI + contracts config⚠️ pytest-oriented evaluable architecture
ArchUnit-style fluent API✅ Yes❌ No❌ No⚠️ Partial
Testing framework integration✅ pytest, unittest, any runner⚠️ CI/pre-commit CLI⚠️ CI/pre-commit CLI⚠️ pytest-focused
Zero runtime dependencies✅ Standard library only⚠️ No app runtime impact, Rust-backed tool❌ Tool dependencies❌ Tool dependencies
Circular dependency detection✅ First-class✅ First-class⚠️ Contract/graph based⚠️ Import-rule based
File/folder dependency rules✅ Glob + regex✅ Module config✅ Import contracts✅ Module rules
Named layer rulesproject_layers()✅ Supported✅ Supported✅ Supported
External dependency rulesdepend_on_external_modules()⚠️ Internal module focus⚠️ Import contract focus⚠️ Internal import focus
TYPE_CHECKING-aware analysis✅ Configurable⚠️ Not the core API⚠️ Not the core API⚠️ Not the core API
Dynamic import detectionimportlib + __import__ string calls⚠️ Not the core workflow⚠️ Not the core workflow⚠️ Import analysis focused
Inline ignore directives# archunit: ignore✅ Supported⚠️ Config-based ignores⚠️ Rule/exclusion based
Naming convention checks✅ Files and paths❌ No❌ No⚠️ Module-name oriented
Code metrics✅ Counts, LCOM, distance metrics❌ No❌ No❌ No
Custom rules and metrics✅ Full support❌ No⚠️ Custom contracts⚠️ Limited custom rule composition
PlantUML diagram validation✅ Supported❌ No❌ No❌ No
Empty test protection✅ Fails by default⚠️ Config validation⚠️ Contract validation⚠️ Not the main focus
Graph/reporting✅ DOT, Mermaid, D2, CSV, JSON, HTML graph reports + metrics HTML✅ DOT, JSON, web graph✅ Browser UI⚠️ Optional graph visualization
Best fitArchitecture tests, CI fitness functions, metrics, diagramsModular monolith dependency governanceConfig-driven import contractspytest import-boundary checks

The most important differences:

  • Test-native by design: ArchUnitPython rules are just Python tests, so architecture decisions are reviewed, run, and debugged in the same workflow as the rest of your test suite.
  • Broader rule surface: dependency direction, cycles, layer policies, external modules, type-only imports, dynamic imports, naming, metrics, custom rules, and PlantUML validation live in one API.
  • False-positive protection: empty checks fail by default, which helps catch typos in file and folder patterns before they silently make your architecture tests meaningless.
  • Quality beyond imports: ArchUnitPython can enforce code metrics such as LCOM cohesion, field/method counts, abstractness, instability, and distance from the main sequence.

📢 Informative Error Messages

When tests fail, you get helpful output with file paths and violation details:

Found 2 architecture violation(s):
1. File dependency violation
'src/api/bad_shortcut.py' depends on 'src/retrieval/vector_store.py'
2. File dependency violation
'src/api/bad_shortcut.py' depends on 'src/retrieval/embedder.py'

📝 Debug Logging & Configuration

We support logging to help you understand what files are being analyzed and troubleshoot test failures. Logging is disabled by default to keep test output clean.

Enabling Debug Logging

fromarchunitpythonimportCheckOptionsfromarchunitpython.common.logging.typesimportLoggingOptionsoptions=CheckOptions(
logging=LoggingOptions(
enabled=True,
level="debug", # "error" | "warn" | "info" | "debug"log_file=True, # Creates logs/archunit-YYYY-MM-DD_HH-MM-SS.log
),
)
violations=rule.check(options)

CI Pipeline Integration

# GitHub Actions
- name: Run Architecture Testsrun: pytest tests/test_architecture.py -v
- name: Upload Test Logsif: always()uses: actions/upload-artifact@v3with:
name: architecture-test-logspath: logs/

🏈 Architecture Fitness Functions

The features of ArchUnitPython can very well be used as architectural fitness functions. See here for more information about that topic.

🔲 Core Modules

ModuleDescriptionStatus
FilesFile and folder based rulesStable
MetricsCode quality metricsStable
SlicesArchitecture slicingStable
GraphDependency graph reportsExperimental
TestingTest framework integrationStable
CommonShared utilitiesStable
ReportsGenerate HTML reportsExperimental

ArchUnitPython uses ArchUnitPython

We use ourselves to ensure the architectural rules for this repository.

🦊 Contributing

We highly appreciate contributions. See Contributing for the full workflow.

  • Use feature branches and open pull requests against main.
  • Use Conventional Commits so releases can be versioned automatically.
  • Do not bump versions manually for normal feature or fix work; semantic-release updates pyproject.toml, src/archunitpython/__init__.py, and CHANGELOG.md.
  • CI checks linting, typing, tests, package builds, and release metadata sync.

ℹ️ FAQ

Q: What Python testing frameworks are supported?

ArchUnitPython works with pytest, unittest, and any other testing framework. We recommend pytest with assert_passes().

Q: What Python versions are supported?

Python 3.10 and above.

Q: Does ArchUnitPython have any runtime dependencies?

No. ArchUnitPython uses only the Python standard library. Development dependencies (pytest, mypy, ruff) are optional.

Q: How does it analyze Python imports?

ArchUnitPython uses Python's built-in ast module to parse source files and resolve imports. It handles absolute imports, relative imports, and package imports.

Q: How do I handle false positives in architecture rules?

Use the filtering and targeting capabilities to exclude specific files or patterns. You can filter by file paths, class names, or custom predicates to fine-tune your rules.

📅 Plans

ArchUnitPython is the Python port of ArchUnitTS. We plan to keep it in sync with the TypeScript version's features, and extend it with Python-specific capabilities.

🐣 Origin Story

ArchUnitPython started as the Python port of ArchUnitTS. With the rise of LLMs and AI integration, enforcing architectural boundaries and QA in general has become more critical than ever -- especially in Python, the dominant language in the AI/ML ecosystem.

💟 Community

Maintainers

Contributors

Questions

Found a bug? Want to discuss features?

If ArchUnitPython helps your project, please consider:

  • Starring the repository 💚
  • Sponsoring development via GitHub Sponsors
  • Suggesting new features 💭
  • Contributing code or documentation ⌨️

Star History

Star History Chart

📄 License

This project is under the MIT license.


Go Back to Top

About

ArchUnitPython is an architecture testing library. Specify and ensure architecture rules in your Python app. Easy setup and pipeline integration.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages