<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title>GUIDE — Python under the Discipline, v0.1</title>
  <p p="1">**Status.** 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.</p>
  <p p="2">Framing 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.</p>
  <p p="3">**Scope 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.</p>
  <section id="baseline" title="0. Language baseline">
    <list ordered="false" p="4">
      <item>**Version floor:** Python 3.12 (PEP 695 native generics: `class Ok[T]`, `type Result[T, E] = …`); target the latest stable.</item>
      <item>**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: ignore` and 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).</item>
      <item>**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.</item>
      <item>**`Any` policy:** banned in cells (strict mode surfaces unknowns); `cast()` and coded ignores are legal only in **boundary modules** with a one-line justification. Prefer `object` + narrowing.</item>
      <item>**Workspace:** **uv** (MIT/Apache-2.0) — environments, `uv.lock` (reproducibility, A2), `[tool.uv.workspace]` as the pnpm/cargo-workspace analog. `pyproject.toml` is the single manifest.</item>
      <item>**Tests:** pytest with `xfail_strict = true` in `pyproject.toml` — the *origin* of the brownfield xfail-strict mechanism, applied globally: a bare lenient `xfail` is impossible; strict xfail markers are the in-source twin of `tests-baseline.json`, and `xtask test-gate` reconciles 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.</item>
      <item>**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.</item>
    </list>
  </section>
  <section id="cells" title="1. Cells">
    <p p="5">A 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.</p>
    <list ordered="false" p="6">
      <item>**Side-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__`, and `TYPE_CHECKING` blocks — 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.</item>
      <item>**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.</item>
      <item>**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).</item>
      <item>Values crossing seams SHOULD be `@dataclass(frozen=True, slots=True)` — immutable messages, cheap and explicit.</item>
      <item>**Promotion to a workspace member** when: heavy optional dependencies, independent publish boundary, or ~2 kLoC.</item>
    </list>
    <p p="7">Cell manifest (decorator carrier, §5):</p>
    <fence lang="python" p="8">@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) -&gt; None: ...</fence>
  </section>
  <section id="seams" title="2. Seams">
    <list ordered="false" p="9">
      <item>A seam is a `typing.Protocol` in 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.</item>
      <item>**Composition over inheritance is a MUST at seams:** no behavior-bearing base classes, no behavior mixins in cells (MRO is action-at-a-distance).</item>
      <item>`@runtime_checkable` MAY be used for registry sanity asserts — with the documented caveat that it checks method *presence*, not signatures; pyright checks the signatures.</item>
      <item>Structural-typing caveat as in TS: accidental conformance is possible; a `ClassVar` brand MAY mark identity-critical seams; the registry remains the gatekeeper.</item>
    </list>
  </section>
  <section id="flags" title="3. Registry and flags">
    <p p="10">R-001 binding — flag at the seam, never in the veins:</p>
    <fence lang="python" p="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) -&gt; 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</fence>
    <list ordered="false" p="12">
      <item>**Two 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: import` guards live only in the registry, never in cells.</item>
      <item>Eager vs lazy cell loading (static import vs `importlib` in the registry) mirrors vibevm's delivery modes; lazy is a registry decision.</item>
      <item>**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 registry `match` is the system's table of contents.</item>
    </list>
  </section>
  <section id="errors" title="4. Errors as contract">
    <p p="13">Python cannot type `raises` — the same A1 hole as TS, in a culture even more exception-centric. The split mirrors Rust's values-vs-panics:</p>
    <list ordered="false" p="14">
      <item>**Expected failures are values at seams.** Minimal core type, no framework dependency:</item>
    </list>
    <fence lang="python" p="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]</fence>
    <p p="16">Error objects carry `code` and the violated REQ URI (`spec: ClassVar[str]`); user-facing rendering appends the URI (PROP-014 §2.6).</p>
    <list ordered="false" p="17">
      <item>**EAFP 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.</item>
      <item>**`raise` is for invariant violations** (the panic analog); never raise bare `Exception`; chain with `raise … from e`.</item>
      <item>**Exhaustiveness:** closed sets are unions handled by `match` with `assert_never(x)` in the default arm — pyright turns Python's unchecked `match` into a compiler-verified one.</item>
      <item>**Async hygiene (MUST):** no un-awaited coroutines (pyright + ruff `ASYNC`); no fire-and-forget `create_task` — unreferenced tasks are garbage-collected mid-flight, Python's most treacherous async landmine; structured concurrency via `asyncio.TaskGroup` (+ `asyncio.timeout`) over bare `gather`/`create_task`.</item>
      <item>**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.</item>
    </list>
  </section>
  <section id="specmark" title="5. specmark carrier">
    <p p="18">Decorators — 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.</p>
    <fence p="19">@spec(implements=&lt;uri&gt;, r=&lt;N&gt;)                  # one edge per decorator; stack them
@spec(deviates=&lt;uri&gt;, r=&lt;N&gt;, reason="…")        # reason mandatory
@verifies(&lt;uri&gt;, r=&lt;N&gt;)                          # on tests
__specmap_scope__ = ("&lt;uri&gt;", &lt;N&gt;)               # module-level inheritance
                                                 # (not __spec__ — importlib owns that name)</fence>
    <p p="20">≤3 edges per item or split (same lint as the siblings).</p>
  </section>
  <section id="naming" title="6. Naming (R-020/R-021 bindings)">
    <list ordered="false" p="21">
      <item>Canonical cell class name is computed: `{Variant}{Seam}` → `SatDepSolver`; hand-written names are linted against the manifest. Length free, ambiguity not.</item>
      <item>**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` / pytest `monkeypatch` in 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).</item>
    </list>
  </section>
  <section id="replacement" title="7. Replacement protocol (R-040 binding)">
    <p p="22">A 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.</p>
  </section>
  <section id="risks" title="8. Risk table (what conform must cover for Python)">
    <table p="23">
      <tr>
        <td>Footgun</td>
        <td>Rule</td>
        <td>Tier</td>
      </tr>
      <tr>
        <td>`Any` / `cast` / coded-ignore outside boundary modules</td>
        <td>§0</td>
        <td>T-syn + pyright</td>
      </tr>
      <tr>
        <td>bare `# type: ignore` / bare `# noqa`</td>
        <td>§0 (PGH003/PGH004)</td>
        <td>T-lex</td>
      </tr>
      <tr>
        <td>un-awaited coroutine / unreferenced `create_task`</td>
        <td>§4</td>
        <td>T-sem</td>
      </tr>
      <tr>
        <td>import-time side effects or registration in cells</td>
        <td>§1</td>
        <td>T-syn</td>
      </tr>
      <tr>
        <td>cell importing a sibling cell</td>
        <td>R-002</td>
        <td>T-syn</td>
      </tr>
      <tr>
        <td>expected failure raised across a seam</td>
        <td>§4</td>
        <td>T-sem</td>
      </tr>
      <tr>
        <td>closed-union `match` without `assert_never` default</td>
        <td>§4</td>
        <td>T-sem</td>
      </tr>
      <tr>
        <td>`mock.patch` / `monkeypatch` in cell tests</td>
        <td>§6</td>
        <td>T-syn</td>
      </tr>
      <tr>
        <td>metaclass / `__getattr__` / subclass-hook registry in cells</td>
        <td>§6</td>
        <td>T-syn</td>
      </tr>
      <tr>
        <td>direct `os.environ` / file / network access in cells</td>
        <td>§1</td>
        <td>T-syn</td>
      </tr>
      <tr>
        <td>dynamic import outside the registry</td>
        <td>§3</td>
        <td>T-syn</td>
      </tr>
      <tr>
        <td>module-level singleton wiring (`X = X()` at import)</td>
        <td>§3</td>
        <td>T-syn</td>
      </tr>
      <tr>
        <td>mutable default argument</td>
        <td>§6 (B006)</td>
        <td>T-syn</td>
      </tr>
      <tr>
        <td>flag read outside the registry</td>
        <td>R-001</td>
        <td>T-syn</td>
      </tr>
      <tr>
        <td>public export without own/inherited spec edge</td>
        <td>PROP-014 §3.2-6</td>
        <td>T-syn + index</td>
      </tr>
    </table>
  </section>
  <section id="docs" title="9. Doc layer">
    <p p="24">Google-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.</p>
    <p p="25">**First 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.</p>
    <p p="26">*Any 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.*</p>
  </section>
</spec>
