Skip to main content

smelt

Static architecture guardrails for Python. Describe your features, layers and roles in smelt.yaml; smelt check reports every import and construct that breaks them, with the exact location, the allowed alternative and a hint on how to fix it.

Installation

uv tool install smelt-cli
# or install into your project's environment:
pip install smelt-cli

The PyPI package is named smelt-cli; the CLI command and Python package are both named smelt. To run without installing, use uvx --from smelt-cli smelt check.

Usage

smelt check                          # whole project, text output
smelt check --changed                # only files changed against HEAD (incl. untracked)
smelt check --changed --base origin/main --format json
smelt context voice                  # architecture briefing for a feature or path
smelt explain SMT101                 # rationale, examples and config knobs of a rule
smelt rules                          # all rules with defaults
smelt debt                           # record today's violations as known debt
smelt debt --prune                   # drop debt entries that were fixed
smelt init                          # draft a config from packages or uv workspace members

smelt debt lets an existing project adopt smelt incrementally: with findings.debt: .smelt/debt.json in smelt.yaml, smelt check only fails on new violations, and SMT903 reports entries that were fixed and can leave the file.

Exit codes: 0 clean, 1 violations at or above --fail-on, 2 config or usage error.

Configuration

smelt.yaml has one section per question:

Section Answers
project Where is the code? (source_roots, test_roots; packages are discovered)
architecture What shape should it have? (features, layers, shared, composition root, cross-feature relationships, cycles, roles)
conventions How is code named, placed and tested? (naming, packages, tests)
integrations Where does a framework get special treatment? (dependency_injection)
analysis How does smelt read the code? (imports, types)
rules How loud is a finding? Severities only, by rule name or code
findings How are existing findings handled? (debt, ignore, suppressions)

smelt config show prints the resolved config with all defaults; docs/configuration.md explains the layout.

For a uv workspace, run smelt init at the workspace root. It reads tool.uv.workspace.members, finds each member's source and test roots, and drafts one configuration for all packages. Review the generated policy before adopting its findings: feature/layer boundaries are inferred, not a declaration of your intended architecture. For example, feature-local DI providers can be allowed to use the DI framework while remaining in their original feature and layer:

architecture:
  features: {root: backend.features}
  composition_root: [backend.main, backend.lifespan]
  cross_feature:
    default: deny
    allow:
      - from: session.presentation
        to: auth.presentation

integrations:
  dependency_injection:
    frameworks: [dishka]  # always allowed in the composition root
    allowed_in: ["backend.features.*.infrastructure.di"]

The object form permits only that directional feature/layer relationship. The shorter "presentation -> presentation" form remains available when a global layer-pair exception is intended. allowed_in entries are dotted module patterns (* is one segment, ** any number) and also cover their submodules; unlike the composition root, those modules keep all feature and layer rules.

For an architecture-first adoption, smelt check --select SMT1,SMT3 focuses on dependency and structure findings. To silence noisier testing rules persistently, use severity overrides such as rules: {private-access: off, interaction-assertion: off} (rule names or codes) and review them later.

Silence a single finding inline, always with a reason:

from gateway.infra.sql import Repo  # smelt: ignore[SMT101] -- migration tracked in #123

With conventions.tests.layout: mirror, every test file must mirror a source module by its path: tests/billing/test_invoice.py needs app/billing/invoice.py, and a package test tests/billing/test_billing.py needs app/billing/. Not every module needs a test, but a test whose source is missing or elsewhere is an error. Deliberately unmirrored tests go in conventions.tests.unmirrored (e.g. ["tests/integration/**"]); conventions.tests.mirror_suffixes: true also allows test_invoice_<topic>.py. conventions.tests.mirror sets the convention relative to the test root: the default {path}/test_{module}.py drops the root package, {root}/{path}/test_{module}.py keeps it, and unit/{path}/{module}_test.py puts tests under tests/unit/ with a suffix.

Every rule has a page under docs/rules, and smelt.schema.json gives editors autocompletion for smelt.yaml.

Python versions

Smelt runs on Python 3.12 to 3.14 and parses your code with the Python it runs on. Code that uses newer syntax (3.14's except A, B: or t-strings, 3.13's type parameter defaults) needs smelt on that version, e.g. uvx -p 3.14 --from smelt-cli smelt check; the syntax error says so when requires-python or .python-version targets a newer Python.

Optional type information

Role detection is nominal by default: a class is an adapter when it inherits a port. With

analysis:
  types: pyright        # needs pyright on PATH; pyright_command overrides how it is run

Smelt also asks pyright whether a class satisfies a port structurally, so a duck-typed adapter is found too. It is never required: without it, every rule still runs.

Using Smelt with coding agents

Add this to your AGENTS.md or CLAUDE.md:

Before implementing, run `smelt context <feature>` to see where code belongs.
After every change, run `smelt check --changed --format json`.
Do not finish while errors remain. Use `smelt explain <code>` when unsure.

Suggested loop: smelt context <feature> → edit → smelt check --changed → fix → tests → pre-commit → CI (full check). Prefer the cheapest verification that gives sufficient confidence: a rename needs smelt plus a type checker, new behavior needs one focused regression test.

pre-commit

repos:
  - repo: https://github.com/mathisarends/smelt
    rev: v0.1.0
    hooks:
      - id: smelt

GitHub Actions

CI always checks the whole repository, because cycles and transitive rules cannot be judged from a diff alone.

- uses: astral-sh/setup-uv@v6
- run: uvx --from smelt-cli smelt check --format github
# optional: code scanning
- run: uvx --from smelt-cli smelt check --format sarif > smelt.sarif || true
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: smelt.sarif

Development

Requires uv.

uv sync                  # create .venv and install dev dependencies
uv run pre-commit install

Common commands:

uv run pytest                        # run tests
uv run pytest --cov                  # run tests with coverage
uv run ruff check --fix .            # lint
uv run ruff format .                 # format
uv run mypy                          # type-check
uv run pre-commit run --all-files    # run all hooks
uv run smelt check                   # smelt checks itself
uv run python scripts/generate.py    # refresh smelt.schema.json and docs/rules/

Commit messages follow Conventional Commits.

Metadata

Release files for smelt-cli 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for smelt-cli 0.1.0
File Size Uploaded
smelt_cli-0.1.0.tar.gz 82.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for smelt-cli 0.1.0
File Interpreter ABI Platform
smelt_cli-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 202.3 kB

Release files / smelt_cli-0.1.0.tar.gz

Download URL smelt_cli-0.1.0.tar.gz
Size 82.4 kB
Tags Source
SHA-256 checksum
How to use checksums
90933e1b354169ca8c5d41daf4c6401fb6fc055fb3b10cefa807f29479dfebc5
BLAKE2b-256 checksum
How to use checksums
28dbfdab10ec8ca7477e58870a6688d061b05cd78f693ca52997cba6b0f1a6df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.2

Release files / smelt_cli-0.1.0-py3-none-any.whl

Download URL smelt_cli-0.1.0-py3-none-any.whl
Size 120.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cdc3062273a0b4e5941b6e3937b82e3d600dbeedcba53f36863c8abaa2d35583
BLAKE2b-256 checksum
How to use checksums
d4c080eb723c4251ce5106f13a38c500644a4bc67e6fd81136e0cd846b5e6e24
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.2

Release history Release notifications | RSS feed

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page