Skip to content

Never-Over/tach

Repository files navigation

image image image image Checked with pyright Ruff

tach

a Python tool to enforce modular design

Docs

Discord - come say hi!

tach_demo.mp4

What is tach?

tach allows you to define boundaries and control dependencies between your Python packages. Each package can also define its public interface.

This enforces a decoupled, modular architecture, and prevents tight coupling. If a package tries to import from another package that is not listed as a dependency, tach will report an error. If a package tries to import from another package and does not use its public interface, with strict: true set, tach will report an error.

tach is incredibly lightweight, and has no impact on your runtime. Instead, its checks are performed as a lint check through the CLI.

Installation

pip install tach

Quickstart

tach comes bundled with a command to set up and define your initial boundaries.

tach init

By running tach init from the root of your Python project, tach will initialize each top-level Python package. Each package will receive a package.yml with a single tag based on the folder name. The tool will take into consideration the usages between packages, and write a matching set of dependencies to tach.yml in the project root.

If you'd like to incrementally or individually add new packages to your tach.yml, you can use:

tach add [file_or_path]

This will create a boundary around the given file or directory, and update your tach.yml with the correct set of dependencies.

Defining Packages

To define a package, add a package.yml to the corresponding Python package. Add at least one 'tag' to identify the package:

# core/package.yml
tags: ["core"]
# db/package.yml
tags: ["db"]
# utils/package.yml
tags: ["utils"]

Next, specify the constraints for each tag in tach.yml in the root of your project:

# [root]/tach.yml
constraints:
- tag: core
  depends_on:
  - db
  - utils
- tag: db
  depends_on:
  - utils
- tag: utils
  depends_on: []

With these rules in place, packages with tag core can import from packages with tag db or utils. Packages tagged with db can only import from utils, and packages tagged with utils cannot import from any other packages in the project.

tach will now flag any violation of these boundaries.

# From the root of your Python project (in this example, `project/`)
> tach check
❌ ./utils/helpers.py: Import "core.PublicAPI" is blocked by boundary "core". Tag(s) ["utils"] do not have access to ["core"].

Defining Interfaces

If you want to define a public interface for the package, import and reference each object you want exposed in the package's __init__.py and add its name to __all__:

# db/__init__.py
from db.service import PublicAPI

__all__ = ["PublicAPI"]

Turning on strict: true in the package's package.yml will then enforce that all imports from this package occur through __init__.py and are listed in __all__

# db/package.yml
tags: ["db"]
strict: true
# The only valid import from "db"
from db import PublicAPI 

Pre-Commit Hook

tach can be installed as a pre-commit hook. See the docs for installation instructions.

Advanced

tach supports specific exceptions. You can mark an import with the tach-ignore comment:

# tach-ignore
from db.main import PrivateAPI

This will stop tach from flagging this import as a boundary violation.

You can also specify multiple tags for a given package:

# utils/package.yml
tags: ["core", "utils"]

This will expand the set of packages that "utils" can access to include all packages that "core" and "utils" depends_on as defined in tach.yml.

By default, tach ignores hidden directories and files (paths starting with .). To override this behavior, set exclude_hidden_paths in tach.yml

exclude_hidden_paths: false

Details

tach works by analyzing the abstract syntax tree (AST) of your codebase. It has no runtime impact, and all operations are performed statically.

Boundary violations are detected at the import layer. This means that dynamic imports using importlib or similar approaches will not be caught by tach.

PyPi Package

License

GNU GPLv3