GUIDE — Python under the Discipline, v0.1
01Status. Beta; third T2 guide. Section structure is isomorphic to GUIDE-RUST-v0.1.md and GUIDE-TYPESCRIPT-v0.1.xml — the guides are meant to be diffed across languages. Scope: only what the axioms require.
02Framing note — the third point of the typology. Rust enforces; TypeScript permits but compiles; Python trusts: annotations are promises kept only by external tools, and at runtime everything is mutable, including an object's class. The discipline's instruments here are therefore the strictest in the set: an external checker as a hard gate, runtime parsing at every boundary, and the hardest bans on dynamism. Patterns dissolve via first-class functions and Protocols: Strategy = Protocol + injected callable; Visitor = union + match + assert_never; GoF Decorator = a wrapper object (and is not the same thing as a Python @decorator — the name collision deceives); Observer = an explicit event seam; Singleton = forbidden, and the module-level instance is precisely the Python form of the disease.
03Scope honesty. This guide governs cells — long-lived system code. Exploratory notebooks, one-off scripts, and data-analysis scratch work are explicitly out of discipline scope; forcing them in would violate A2's economics. Framework surfaces (web routes, CLI entry points, fixtures) are boundary modules (§1) that call cells, not cells themselves.
0. Language baseline
- 04Version floor: Python 3.12 (PEP 695 native generics:
class Ok[T],type Result[T, E] = …); target the latest stable. - Type gate: pyright,
typeCheckingMode = "strict"— the single gating checker (two masters disagree; mypy MAY run advisory as a second evidence provider). Load-bearing settings:reportUnnecessaryTypeIgnoreComment = "error",enableTypeIgnoreComments = false— together they ban bare# type: ignoreand make every surviving# pyright: ignore[ruleId]xfail-strict: when the underlying error disappears, the suppression itself fails, forcing promotion (BROWNFIELD §4 at the type level). - Lint/format: ruff (MIT; written in Rust — a pleasing T3 synergy) for both. Load-bearing rule families:
B(bugbear),UP,I,ASYNC,RUF(incl. RUF100 unused-noqa),PGH(PGH003/PGH004 ban blanket# type: ignore/ blanket# noqa) — every suppression carries its code or fails. Anypolicy: banned in cells (strict mode surfaces unknowns);cast()and coded ignores are legal only in boundary modules with a one-line justification. Preferobject+ narrowing.- Workspace: uv (MIT/Apache-2.0) — environments,
uv.lock(reproducibility, A2),[tool.uv.workspace]as the pnpm/cargo-workspace analog.pyproject.tomlis the single manifest. - Tests: pytest with
xfail_strict = trueinpyproject.toml— the origin of the brownfield xfail-strict mechanism, applied globally: a bare lenientxfailis impossible; strict xfail markers are the in-source twin oftests-baseline.json, andxtask test-gatereconciles both. Property testing: hypothesis — license note: MPL-2.0, the Charter's case-by-case zone; verdict acceptable: test-only dependency, never linked into shipped artifacts. Flagged here so the exception is conscious, not accidental. - Boundary validation (parse, don't validate): types are erased harder than in TS — there is not even a compile step. Every external input (network, disk, env, subprocess output) crosses through a typed parsing model at the boundary — pydantic (MIT) primary. Boundary models are the taggable schema units (the JTD parallel from vibevm): the model carries the spec edge; inferred types flow inward from it.
1. Cells
05A cell is a package (directory) behind one seam: __init__.py is the single public surface, with an honest, mandatory __all__ exporting the seam implementation and nothing else.
- 06Side-effect-free on import — the hardest rule in the hardest culture. Module-level execution is idiomatic Python; in cells it is banned: top level admits imports,
def/class, constants,__all__,__specmap_scope__, andTYPE_CHECKINGblocks — no I/O, no global mutation, and no registration-on-import (the@register-at-import / autodiscovery pattern moves to the composition root). Enforced: T-syn top-level statement whitelist. - No sibling-cell imports (R-002) — Python's circular-import hell makes the rule doubly valuable. Cross-seam type references via
if TYPE_CHECKING:are fine and encouraged. - Platform capabilities are injected. Cells never touch
os.environ, filesystem I/O, HTTP clients directly — those are Protocol seams passed at construction. Time and randomness SHOULD also be injected (deterministic tests). Consequence: cell tests need no patching at all (§6). - Values crossing seams SHOULD be
@dataclass(frozen=True, slots=True)— immutable messages, cheap and explicit. - Promotion to a workspace member when: heavy optional dependencies, independent publish boundary, or ~2 kLoC.
07Cell manifest (decorator carrier, §5):
08@spec(implements="spec://org.vibevm.core/vibevm/modules/vibe-resolver/PROP-003#solver-upgrade", r=2)
@cell(seam="DepSolver", variant="sat", replaces="naive", flag="solver")
class SatDepSolver:
def __init__(self, provider: DepProvider) -> None: ...
2. Seams
- 09A seam is a
typing.Protocolin core/seams — Protocol over ABC, deliberately: ABCs invite inheritance and template methods (hidden control flow, R-021); Protocol is pure shape — checkable structure with zero runtime coupling, at peace with duck-typing culture. - Composition over inheritance is a MUST at seams: no behavior-bearing base classes, no behavior mixins in cells (MRO is action-at-a-distance).
@runtime_checkableMAY be used for registry sanity asserts — with the documented caveat that it checks method presence, not signatures; pyright checks the signatures.- Structural-typing caveat as in TS: accidental conformance is possible; a
ClassVarbrand MAY mark identity-critical seams; the registry remains the gatekeeper.
3. Registry and flags
10R-001 binding — flag at the seam, never in the veins:
11# src/registry.py — the only module reading selection flags and the only
# legal site of dynamic import (importlib with computed names).
def dep_solver(flags: Flags, provider: DepProvider) -> DepSolver:
match flags.get("solver"): # provenance: default | env | cli | lockfile
case "sat":
mod = importlib.import_module("cells.sat_dep_solver") # lazy delivery
return mod.SatDepSolver(provider)
case _:
return NaiveDepSolver(provider) # eager
- 12Two tiers, never confused: optional-dependency extras answer "is the code in the environment" — the cargo-feature analog; runtime flags answer "is the cell selected".
try: importguards live only in the registry, never in cells. - Eager vs lazy cell loading (static import vs
importlibin the registry) mirrors vibevm's delivery modes; lazy is a registry decision. - No DI frameworks, no module-level singleton wiring. The
settings = Settings()-at-import pattern is the Python singleton disease: config objects are constructed at the composition root and passed down. Explicit constructor injection + the registrymatchis the system's table of contents.
4. Errors as contract
13Python cannot type raises — the same A1 hole as TS, in a culture even more exception-centric. The split mirrors Rust's values-vs-panics:
- 14Expected failures are values at seams. Minimal core type, no framework dependency:
15 @dataclass(frozen=True, slots=True)
class Ok[T]: value: T
@dataclass(frozen=True, slots=True)
class Err[E]: error: E
type Result[T, E] = Ok[T] | Err[E]
16Error objects carry code and the violated REQ URI (spec: ClassVar[str]); user-facing rendering appends the URI (PROP-014 §2.6).
- 17EAFP stays legal inside a cell — the official idiom is not repealed; the seam surface is where failure must be a value, because that is the only way the failure set becomes part of the checked interface.
raiseis for invariant violations (the panic analog); never raise bareException; chain withraise … from e.- Exhaustiveness: closed sets are unions handled by
matchwithassert_never(x)in the default arm — pyright turns Python's uncheckedmatchinto a compiler-verified one. - Async hygiene (MUST): no un-awaited coroutines (pyright + ruff
ASYNC); no fire-and-forgetcreate_task— unreferenced tasks are garbage-collected mid-flight, Python's most treacherous async landmine; structured concurrency viaasyncio.TaskGroup(+asyncio.timeout) over baregather/create_task. - Provenance note: tracebacks resolve to real source lines natively — the release-map problem is easier than in JS; if shipping frozen/compiled distributions, retain line tables so the A1 chain survives.
5. specmark carrier
18Decorators — and this is a deliberate asymmetry with the TS guide, where decorators were rejected: a Python decorator is a plain function with zero new semantics, costs one call at class creation, works on every .py (there is no untyped sibling language), and is parseable by T-syn while also attaching an introspectable __specmark__ attribute — runtime tooling can read the edges, a capability Rust and TS carriers lack.
19@spec(implements=<uri>, r=<N>) # one edge per decorator; stack them
@spec(deviates=<uri>, r=<N>, reason="…") # reason mandatory
@verifies(<uri>, r=<N>) # on tests
__specmap_scope__ = ("<uri>", <N>) # module-level inheritance
# (not __spec__ — importlib owns that name)
20≤3 edges per item or split (same lint as the siblings).
6. Naming (R-020/R-021 bindings)
- 21Canonical cell class name is computed:
{Variant}{Seam}→SatDepSolver; hand-written names are linted against the manifest. Length free, ambiguity not. - Forbidden in cells regardless of elegance — the Python theater list: metaclasses in domain code (infra crates only — the proc-macro parallel);
__getattr__/__getattribute__dynamic dispatch; monkey-patching anything;unittest.mock.patch/ pytestmonkeypatchin cell tests — patching through the module graph is action-at-a-distance, and capability injection (§1) makes it unnecessary; import-time registration hooks (__init_subclass__,__set_name__registries) in domain code;eval/exec;globals()/setattr-loop object surgery; descriptors with side effects; star imports; mutable default arguments (ruff B006 — the classic).
7. Replacement protocol (R-040 binding)
22A cell with replaces=… ships a differential oracle: hypothesis property tests asserting agreement with the old cell across the seam (documented-divergence list otherwise), @verifies-tagged. Snapshot artifacts (syrupy-class tools) follow the promotion protocol — CI never updates; local updates carry a debt/intent reference. With xfail_strict = true global, a healed known-failing test breaks the gate until promoted — the registry shrinks truthfully by construction.
8. Risk table (what conform must cover for Python)
| Footgun | Rule | Tier |
|---|---|---|
Any / cast / coded-ignore outside boundary modules |
§0 | T-syn + pyright |
bare # type: ignore / bare # noqa |
§0 (PGH003/PGH004) | T-lex |
un-awaited coroutine / unreferenced create_task |
§4 | T-sem |
| import-time side effects or registration in cells | §1 | T-syn |
| cell importing a sibling cell | R-002 | T-syn |
| expected failure raised across a seam | §4 | T-sem |
closed-union match without assert_never default |
§4 | T-sem |
mock.patch / monkeypatch in cell tests |
§6 | T-syn |
metaclass / __getattr__ / subclass-hook registry in cells |
§6 | T-syn |
direct os.environ / file / network access in cells |
§1 | T-syn |
| dynamic import outside the registry | §3 | T-syn |
module-level singleton wiring (X = X() at import) |
§3 | T-syn |
| mutable default argument | §6 (B006) | T-syn |
| flag read outside the registry | R-001 | T-syn |
| public export without own/inherited spec edge | PROP-014 §3.2-6 | T-syn + index |
9. Doc layer
24Google-style docstrings (ruff D family) on every tagged export: error codes and their REQ URIs, async semantics, edge cases, performance traps. The docstring is the human-facing detail layer — and, Python bonus, a runtime-introspectable one (help()); the spec stays thin; the ledger renders from both. Duplication between docstring and spec is a defect on the spec side.
25First carrier note. The first Python cell under this guide is designated by the architecture itself — the conform engine's CPython ast/symtable sidecar (ENGINE §2): the process that asks Python about its own language. Self-hosting, third time.
26Any rule binding here without a corresponding conform check (or explicit WISH mark in the Charter rule record) by the first Python carrier milestone is removed rather than carried as aspiration.