Skip to content

Tach: Architecture Boundaries

Tool: Tach

Tach is an architectural boundary tool for Python written in Rust. It enforces strict layering and prevents circular dependencies. Deptry checks third-party dependency declarations.

What It Does

Tach evaluates the import graph of a Python repository to enforce modular boundaries and eliminate circular dependencies. It defines explicit, unidirectional boundaries between layers—preventing low-level models and core logic from coupling to high-level CLI or API orchestrators.

Tach can generate visual Mermaid diagrams directly from the verified import graph. The dependencies Poe task runs tach check for internal boundaries and deptry src for declared dependencies.

Why It's in Zenith

Over time, codebases naturally suffer from architectural erosion as developers and AI assistants import utilities opportunistically across modules, eventually creating an untestable monolith. Zenith integrates Tach to enforce clear layer boundaries and catch circular references at commit time. Deptry checks that package imports match declared dependencies.

Configuration in Zenith

Configured in tach.toml at the project root:

# ==============================================================================
# Tach Configuration (tach.toml)
#
# Tool: Tach (https://github.com/gauge-sh/tach)
# Purpose: Modular architecture and dependency boundary enforcement for Python.
# Why it's in Zenith: Enforces architectural boundaries between package modules
# (e.g. CLI layer depends on Core, but Core must never depend on CLI) and prevents
# circular dependencies from creeping in.
# ==============================================================================

exclude = [
    "**/*__pycache__",
    "**/*egg-info",
    "**/docs",
    "**/tests",
    "**/venv",
    "**/.venv",
    "**/site",
]
interfaces = [  ]
source_roots = [ "src" ]

exact                        = true
forbid_circular_dependencies = true
ignore_type_checking_imports = true
{% if cookiecutter.include_cli in [True, "yes", "true", "True", "1"] %}
layers                       = [ "cli", "core" ]
{% else %}
layers                       = [ "core" ]
{% endif %}
respect_gitignore            = true

[rules]
unused_ignore_directives         = "error"
require_ignore_directive_reasons = "error"


[[modules]]
    depends_on = [  ]
    layer      = "core"
    path       = "{{ cookiecutter.package_name }}"

{% if cookiecutter.include_cli in [True, "yes", "true", "True", "1"] %}
[[modules]]
    depends_on = [ "{{ cookiecutter.package_name }}.cli" ]
    layer      = "cli"
    path       = "{{ cookiecutter.package_name }}.__main__"

[[modules]]
    depends_on = [  ]
    layer      = "cli"
    path       = "{{ cookiecutter.package_name }}.cli"
{% endif %}

Boundary Rules

Lower-level domains such as core and models must never import higher-level orchestration layers like cli. All cross-module interactions must pass through explicit, public interfaces.

Key Invocations

# Verify module architecture boundaries.
uv run tach check

# Verify declared dependencies.
uv run deptry src

# Run the configured architecture task.
uv run poe dependencies

# Export a Mermaid architecture diagram.
uv run tach show --mermaid -o docs/assets/architecture.mmd

Upstream Documentation