Architecture Best Practices
Standing rules for Python services that stay testable, deployable, and understandable as frameworks and teams change.
Search across all documentation pages
Standing rules for Python services that stay testable, deployable, and understandable as frameworks and teams change.
Protocol for outward dependencies. Repositories, clocks, and notifiers get explicit interfaces.main, lifespan, or build_container() wires concrete adapters - not scattered globals.uv sync or pip install -e . before tests.PYTHONPATH=. hacks in documented dev workflows. If imports need it, packaging metadata is wrong.__all__ or documented package exports. Internal modules stay refactorable.os.getenv in business modules..env.example lists names, never values.SECRET_KEY values must fail fast in production settings.place(), cancel()). Public attribute mutation bypasses invariants.print debugging does not scale to production.Scripts under a few hundred lines may colocate logic and IO. Promote to packages with boundaries when a second entry point appears or the script survives a quarter.
No. You need clear dependency direction. Folder names matter less than "domain does not import FastAPI."
When accessed only from composition roots and adapters. Use cases receive values or protocols, not from app.settings import settings.
Use it in design review and quarterly audits. Daily PRs reference specific rules ("domain import added - rejected") instead of rerunning the full list.
Django projects still benefit from ports for external IO and typed settings. ORM models may stay fat for admin-heavy CRUD; add mapping when rules multiply.
In-memory repositories and logging mailers/notifiers cover most use cases. Add contract tests when adapter SQL is complex.
Tools like import-linter or custom ruff rules can forbid domain importing adapters. Wire into CI after conventions stabilize.
No. A well-bounded monolith with clear modules often outperforms distributed sprawl. Split on team or scale triggers, not fashion.
For decisions costly to reverse: datastore, auth model, event bus, major framework. Skip ADRs for obvious tool picks like ruff.
Settings validate environment. Domain validates business rules. Overlap is rare - keep layers separate.
Notebooks are throwaway or promotion candidates. Do not let experimental imports become de facto production boundaries without packaging.
uv 0.6+ lockfiles make CI and Docker reproduce the same dependency graph - a foundation for evolvable design.
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 19, 2026