Well-Structured Python at Scale

Python programs stay small and single-purpose not because the language is a “scripting language” but because nothing in it enforces structure. Large, well-structured Python is Python plus a constitution plus checkers — the structure is imposed by convention and tooling, and the industry has converged on what both look like.

Links: Practicality vs. Precision — Principled LLM Code — the general thesis (accretion → big ball of mud; persistence is the fix); this page is its Python instance, Repairing LLM Code — The Two Oracles, Conservation of Complexity, Camelot From YouTube — first specimen, dj-score — the specimen’s target library, CyborgDJ, emu-harness — a plug-in shaped tool (backends × machines)

1. Limits → causes → remedies

The limit is not the language. Instagram’s server is “a several-million-line Python monolith” (Meyer, Strict Modules, 2019); Kraken’s Django monolith is 27,637 modules worked on by ~400 developers (Seddon, EuroPython blog, 2023); Dropbox typed “over 4 million lines” (Lehtosalo, 2019).

The cause is the absent enforcement. No compiler checks types; every module is importable from everywhere; nothing is private; a file can grow without limit. A 50-line script and a 2M-line system are written identically — so the default trajectory is Foote & Yoder’s Big Ball of Mud (PLoP 1997; PLoPD4, 2000 — laputan.org/mud). Cosmic Python’s framing: “A big ball of mud is the natural state of software.” The symptom is “sameness of function” — UI handlers doing domain logic, logic classes doing I/O.

The remedy is to supply what the language omits — each row below replaces a missing language feature with a convention + a checker:

Missing from the language Supplied by Checker
a package boundary one installable package, src layout, pyproject.toml (PEP 621) the install itself
a compiler / type system type hints + @dataclass instead of dicts mypy / pyright in CI
interfaces typing.Protocol ports (PEP 544) — structural, no inheritance the type checker
visibility / layering dependency direction (domain ← services ← adapters/entrypoints) import-linter, tach
an extension mechanism registry + plugins; entry points (PyPA spec) the loader
a spec the test suite (fakes, not mocks) pytest

2. What it looks like

myapp/
  pyproject.toml        # metadata, deps, tool config — one place
  src/myapp/            # src layout: tests can't import the working copy by accident
    __init__.py         # the PUBLIC API — re-export what callers may use
    domain/             # pure logic: dataclasses, rules, no I/O
    services/           # use cases: one function per user action, orchestrates domain via ports
    adapters/           # the outside world: files, DB, HTTP, audio, emulators
    entrypoints/        # CLI / GUI / web — thin: parse input, call a service
    bootstrap.py        # composition root — the ONE place concrete adapters get wired in
    config.py           # env-derived values, read via functions; only bootstrap imports it
  tests/unit/  tests/integration/  tests/e2e/

This matches Cosmic Python’s template (domain / service_layer / adapters / entrypoints) with bootstrap.py from ch. 13. Why src/: the current directory is first on sys.path, so a flat layout silently imports the dev copy and hides packaging mistakes (Packaging Guide).

The one rule that matters most: dependencies point inward. domain imports nothing from the outer layers. Circular imports are usually a symptom of violating this — a boundary bug, not something to patch with a function-local import.

3. Cosmic Python, distilled — and when not to

Architecture Patterns with Python (Percival & Gregory, 2020; free online). Thesis: dependency inversion — business logic and infrastructure depend on abstractions, so each can change independently; Flask/SQLAlchemy become “minor implementation details.” The book is unusually honest about cost — every chapter ends in a pros/cons table, and the repeated warning is simple CRUD doesn’t need this.

Pattern Solves Skip it when
Domain model rules scattered and unreadable logic is trivial
Functional core, imperative shell (ch. 3) I/O tangled with decisions; mock.patch tests — (always cheap)
Service layer (ch. 4) no single home for use cases; HTTP/UI holding logic controllers can hold it (“fat models, thin controllers”)
Repository / Unit of Work storage coupled to logic; transaction boundaries implicit the ORM already abstracts it; no persistence to speak of
Aggregate invariants across many objects; concurrency single-user, no shared state
Events + message bus (ch. 8–9) side-effect “goop” (email, logging) wedged into features flow becomes implicit and hard to trace
CQRS (ch. 12) reads and writes with different needs reads and writes share concepts
DI via bootstrap.py hidden dependencies, import-time globals — frameworks only pay off with deep dependency chains

Heuristics worth keeping: a function whose description needs “and” or “then” has two responsibilities; “when X, then Y” phrasing is an event. “Make the change easy; then make the easy change.” Fakes over mocks — if an abstraction is hard to fake, it is too complicated. Every line of test code is coupling.

4. Getting there from an existing mess

From the epilogue — the part that matters for a pile of one-off tools:

  1. Start from the problem, not the pattern — hard to change? slow? buggy? Pay it as an “architecture tax” on feature work.
  2. List the use cases (every UI action, every CLI command), give each an imperative name, and give each a single home in a service layer. Prefer duplication over use cases calling use cases.
  3. Move I/O out of the core into those use cases (functional core, imperative shell). Get legacy code under test first (Feathers).
  4. Extract by copy → repoint → retire — never edit in place across a boundary.
  5. Small steps, no ocean-boiling — pick a specific problem, take local wins, move on if it’s too hard (advice credited in the epilogue to David Seddon — same name as import-linter’s author; not checked that the book means the same person).
  6. Appendix C is the proof of payoff: once the service layer existed, adding a new CSV-driven CLI reused domain and service code unchanged.

5. Exemplars (grounded 2026-10-21)

6. Anti-patterns (the smells of accretion)

7. Specimens

Open Questions

Tags

python, software-engineering