| name | python-values-failure |
| description | Fires when domain data is about to be shaped wrong instead of as the matching construct in python-values-success — a bare primitive field, an unproven scalar or collection, a T | None absence, free-floating vocabulary, a drifted or unmodeled enum, an after-validator policing a relation the fields should reparameterize, a bare .root held mid-graph, an open (unfrozen or extra-allowing) model, a union alias built without a discriminator, a union wrapped in a class, or a field the ontology never declared. |
Values — failure patterns
Thirteen ways domain data gets shaped wrong instead of built as a semantic scalar, value object, concept model, collection, or union (python-values-success).
Bare primitive
A primitive field names a domain quantity but keeps none of its constraint; the constraint then scatters into checks instead of living in the type.
The atomic domain value is a frozen RootModel[P] over one allowed primitive (str, int, float, Decimal, bool, bytes, date), carrying a Field(...) constraint, or a docstring stating why the open range is the domain fact. The field is typed as that scalar, never the primitive; a raw value passed where the scalar field stands constructs the scalar inside the composite's own call.
class Account(BaseModel):
email: str
Scalar not proven by construction
A semantic scalar declared as a plain class or typedef proves nothing when built. Only a frozen RootModel carrying its constraint on the root field makes the invalid value unconstructable.
The required form is class X(RootModel[P], frozen=True) over one allowed primitive, with the domain bound on root's Field(...), or a docstring stating why the open range is the domain fact when the value is unconstrained.
class Price(BaseModel):
value: Decimal
Free-floating vocabulary
A closed value-space with no scalar that owns it: a raw Literal[...] string used as a field, or a StrEnum used directly as a field type. Nothing single proves membership.
A closed vocabulary is a frozen RootModel over a StrEnum or Literal value space; the StrEnum is the value space, the role a gt=0 plays for a number. The field is then typed as that scalar, which proves membership on construction.
status: Literal["open", "filled", "canceled"]
ctx: BoundedContext
Mutable bag
A bare list, set, or dict field is unproven and mutable: nothing guarantees its contents are valid, and it can change after the value is built.
A sequence that is itself a domain thing, with its own name, constraint, or derivation, is a frozen RootModel[tuple[T, ...]], constructed whole in one expression. A namespace whose key-uniqueness is the domain fact is a frozen RootModel[dict[K, V]] paired with a query model returning a found-or-missing union. A sequence with no meaning of its own is a tuple[T, ...] field on the model, not a bare container.
class Book(BaseModel):
bids: list[Level]
Collection not proven by construction
The scalar failure for sequences: an aggregate that is not a frozen RootModel[tuple[T, ...]] is mutable and unproven, its contents guaranteed by nothing and free to change after the value is built.
A sequence that is its own domain thing is a frozen RootModel[tuple[T, ...]] whose element T is a declared type, carrying its bound on Field(...), constructed whole in one expression. A sequence with no name, constraint, or derivation of its own is a plain tuple[T, ...] field on a model.
class Trades(BaseModel):
items: list[Trade]
Nullable absence
A T | None field models absence as a slot modifier instead of a state. "Missing" is a real fact about which world the value is in, and fusing it into one field forces every reader to branch and names neither state.
"May be missing" is never a field. Absence that means something is a union variant named for what absence means, or separate models when absence changes the state shape. When constructing from foreign data, an omitted key resolves to a default that states what omission means, or a variant when omission means a different fact; bare None never crosses into the domain.
class Order(BaseModel):
settled_at: datetime | None
Open product
A value, concept, foreign, or contract model left unfrozen can mutate after construction; one that does not forbid extras silently absorbs unmodeled fields. Either way it stops being a fixed, fully-described fact.
Every product type carries model_config = ConfigDict(frozen=True, extra="forbid"). The consistency model is the one exception, because it is the live node that accumulates state; freezing it is its own violation (see python-state-failure).
class SignalSnapshot(BaseModel):
spread: Spread
Union alias built wrong
The choice is declared without making selection provable: no Annotated, variants that do not match the model, a missing discriminator, or for an ordered union a missing union_mode="left_to_right", wrong variant order, or no TypeAdapter constructor. Selection then leans on inference.
Identity-carrying data renders Annotated[A | B, Field(discriminator="kind")], each variant pinning its kind member, so the checker narrows through the discriminator. Foreign data expected to fail renders Annotated[A | B, Field(union_mode="left_to_right")] with variants in attempt order, the failure variant last, and a TypeAdapter constant named for the alias.
SignalUnion = A | B
Union as a class
Wrapping a choice in a class invents a node for what is purely a type alias. The choice is selected by discriminator or ordered attempt, never by a class body.
A union is a closed set of frozen variants over one axis, rendered as one alias: Annotated[A | B, Field(discriminator="kind")] for identity-carrying data, or Annotated[A | B, Field(union_mode="left_to_right")] for foreign data expected to fail. The alias is the union's one form; a class around it is a structure created only to provide model_validate the alias already provides.
class SignalUnion(BaseModel): ...
Field the ontology didn't model
Class and catalog disagree. An extra field is unmodeled meaning entering source; a missing field is modeled meaning never rendered; a wrong annotation breaks the edge. The ontology row is the authority for the field set and each field's type.
The class carries exactly the fields its row declares, each with the declared type. A field the code needs that no row names is a meaning to place in the ontology first, not to add to the class.
class SignalSnapshot(BaseModel, frozen=True, extra="forbid"):
spread: Spread
urgency: int
Unmodeled or drifted enum
An enum that is neither a scalar's value space nor a union's axis carries vocabulary the catalog does not recognize. One whose members differ from the modeled set has drifted from what it mirrors.
A StrEnum exists in exactly two roles: the value space a frozen RootModel scalar wraps, or the axis a union's variants pin with kind: Literal[Axis.MEMBER]. Its members are exactly the modeled set; a member no row names is vocabulary the ontology never declared.
class Venue(StrEnum):
COINBASE = "coinbase"
Bare .root mid-graph
.root unwraps a RootModel to its bare inner primitive, dropping the proof the scalar or collection carried. The unwrap is innocent in itself; what the program does with the bare value next is what this anti-pattern names. The violation is a held unwrap: a .root whose primitive is kept inside the program rather than immediately fed onward. Held is a closed set of three structural homes:
- bound to a name —
px = self.quote.price.root (the proof is dropped a statement early and carried as a loose primitive),
- operated on as a raw value —
self.bid.root + self.ask.root (the proof is consumed by arithmetic or comparison, not by a construction),
- handed to a method or non-constructing call —
self.bus.publish(self.spread.root) (the bare value is pushed to I/O instead of the binding serializing a whole proven value at the wire).
Everything else an unwrap can do is consumed, not held, and is not a violation. A .root fed straight into a construction is consumed by it: an argument to a Type call, a key or value in a mapping being built, an element splatted into a collection, a comprehension source, an f-string that feeds a construction. So is a .root at the wire edge (a route reply under api/, a client binding under service/), where unwrapping to a bare primitive is the legal crossing out of the program. These are legal because they are simply not a held unwrap, not because of an exception carved out for them.
@cached_property
def snapshot(self) -> Snapshot:
price = self.quote.price.root
return Snapshot(price=Price(price))
@cached_property
def tag(self) -> Tag:
return Tag(self.symbol.root)
After validator
A @model_validator(mode="after") runs once the object is already built, so it can only police a value the structure let exist. Its presence is a question, never a fix: what did the model leave unmodeled, so that a fact has to be checked at runtime instead of carried by the type? Move that fact into the structure, so the invalid state cannot be constructed at all.
- A cross-field relation (
bid <= ask) reparameterizes to a base and a non-negative offset, making the second field a derivation.
- A single-field bound belongs on the scalar's own
Field.
- An "exactly one of these" is a union selected by construction.
There is no legal after-validator: every one names something that was not modeled.
@model_validator(mode="after")
def check(self):
if self.bid > self.ask:
raise ValueError
return self