Skip to content

Specs

A Spec is an immutable, slotted Python record whose field contract is declared with annotations.

from talea import Spec, field


class Customer(Spec):
    customer_id: int
    name: str
    tags: list[str] = field(default_factory=list)

Declaration contract

  • construction is keyword-only;
  • an annotation without a default is required, even when it accepts None;
  • static defaults are validated at class declaration and reused;
  • mutable defaults require field(default_factory=...);
  • field bindings cannot be assigned or deleted after construction;
  • nested mutable values remain ordinary Python objects and are not deep-frozen;
  • exact field order, overrides, hooks, aliases, and metadata are retained in canonical declaration truth.

Spec supports single inheritance and one state-bearing slot lineage in multiple inheritance. Generic declarations use Python 3.14 syntax and execute only after concrete specialization.

Methods and properties

Surface Purpose
from_mapping() Construct from an untrusted Python Mapping
from_json() Decode JSON and construct
to_dict() Detached Python mapping projection
to_json() JSON projection and encoding
json_schema() Draft 2020-12 schema
openapi_schema() OpenAPI 3.1-compatible Schema Object/components fragment
present_fields Supplied canonical names on a partial derived Spec
copy.replace() Python-native validated immutable update

Copying and deep copying preserve ordinary Python semantics. Pickle works for importable declarations and acyclic instances; dynamic or local classes must follow Python's normal importability rules. Cyclic runtime graphs are rejected by validation or serialization rather than represented.

Continue with fields and defaults, composition and inheritance, and immutable updates.

From declaration to committed instance

When Python creates a concrete Spec class, Talea resolves annotations, validates defaults and factories, composes inherited fields/hooks, and produces canonical declaration truth. Construction then evaluates supplied/defaulted values in field order, runs transforms, structural checks, field checks, and complete Spec checks, and publishes the immutable object only after every stage succeeds.

from typing import Annotated

from talea import Alias, MinLength, Sensitive, Spec


class Credentials(Spec):
    token: Annotated[str, Sensitive(), MinLength(16)]


class Account(Spec):
    account_id: int
    display_name: Annotated[str, Alias("displayName"), MinLength(1)]
    credentials: Credentials

Trusted Python construction uses account_id and display_name. External Mapping/JSON input uses displayName, can build the nested Credentials from an object, and applies finite traversal policy. Output uses aliases by default. Those are operations over one Account contract, not interchangeable aliases.

Defaults, nullability, and absence

field: T | None controls whether None is a valid value. field: T = value controls whether ordinary construction may omit the argument. A presence-aware derived Spec controls whether an external partial may omit the field without materializing a source default. These are three independent questions.

Mutable defaults require field(default_factory=...) so each instance gets its own value. Factories are trusted application callbacks, run once for an omitted ordinary field, and are never executed merely to generate JSON Schema.

Nested mutability and replacement

The Spec binding is frozen; nested lists and dictionaries are not deep-frozen. If application code mutates a child, later serialization or replacement revalidates current state where needed. Use copy.replace() to create a validated complete update and apply_patch() when change presence came from an external partial contract.

Immutability does not imply value equality, deep copy, persistence identity, or thread safety for nested mutable application objects. Choose those policies at the domain layer.

Failures, security, and schemas

Invalid construction raises ValidationError with a canonical field path and stable code. Sensitive fields redact Talea-owned repr and failures. Successful serialization still follows the declared output; a separate response Spec is the safest allow-list when request credentials must never leave the service.

json_schema() and openapi_schema() project aliases, requiredness, constraints, metadata, recursion, and tags from the finalized declaration. Arbitrary transform/serializer domains can make one mode unprojectable rather than producing a false schema.

Performance and when not to use Spec

Declaration is cold work and the specialized constructor is reused. Features that are absent from a Spec should impose approximately zero hot-path cost. Nested mutable data and used hooks necessarily add their direct validation work.

Use a dataclass or attrs when you need a plain internal record. A stdlib dataclass can also use Contract(DataclassType) for Talea boundary behavior without becoming a Spec. Use Contract when the useful root is already a container, union, alias, TypedDict, dataclass, or primitive. Use direct Python when the contract is a small specialized predicate. Spec is most useful when nominal immutable record behavior and multiple boundary operations belong together.