Dynamic Specs¶
create_spec creates a normal Spec class from trusted runtime declarations. It
does not create a second execution path.
Dynamic Spec creation¶
create_spec() returns a normal Talea Spec subclass. Its ordered fields
mapping contains evaluated annotations; defaults and factories use separate
unambiguous mappings.
from talea import Title, create_spec
ConfiguredEvent = create_spec(
"ConfiguredEvent",
{
"event_id": str,
"attempt": int,
"labels": list[str],
},
defaults={"attempt": 1},
factories={"labels": list},
module="application.contracts",
doc="Event contract generated from application-owned configuration.",
metadata=(Title("Configured event"),),
)
event = ConfiguredEvent(event_id="evt-1")
The separate mappings avoid tuple ambiguity: (int, 1) may itself be a value
or part of a valid type expression in Python APIs. Each factory must be a
zero-argument callable and runs once per omitted field.
Inheritance¶
The base must be a Spec class or a concrete generic Spec specialization.
Fields are not copied: the existing metaclass and canonical inheritance owner
compose them.
from talea import Spec, create_spec
class Person(Spec):
name: str
Employee = create_spec(
"Employee",
{"employee_id": int},
base=Person,
)
employee = Employee(name="Ada", employee_id=7)
Fresh open generic templates are not dynamically declared in this release.
Use class syntax for a generic declaration, specialize it, then pass the
concrete specialization as base when needed.
Aliases, constraints, methods, and hooks¶
Aliases and constraints remain ordinary Annotated metadata. Methods,
descriptors, @transform, @check, and @serialize callbacks are supplied as
normal trusted namespace entries; there is no parallel hook metadata API.
from typing import Annotated
from talea import Alias, Ge, check, create_spec, serialize
@check("amount")
def nonzero(amount: int) -> None:
if amount == 0:
raise ValueError("amount must be non-zero")
@serialize("amount")
def as_text(amount: int) -> str:
return str(amount)
ConfiguredPayment = create_spec(
"ConfiguredPayment",
{"amount": Annotated[int, Alias("amountCents"), Ge(0)]},
namespace={"nonzero": nonzero, "as_text": as_text},
)
Namespace contributions are trusted application code, just like a class body. They still pass the normal Talea signature, target, inheritance, and lifecycle checks.
Identity, security, and pickle¶
name, module, and qualname accept normalized Python identifiers only.
Field names use the same metaclass validation as static Specs. Names,
annotations, aliases, and hook identities are never interpolated as executable
source. String annotations are rejected: dynamic callers must provide evaluated
runtime annotations.
create_spec sets __name__, __qualname__, __module__, and __doc__, but
does not mutate sys.modules or bind the class for the caller. Standard pickle
rules therefore apply: bind a dynamic class at its declared importable module
and qualified name before expecting instances to pickle.
Python cannot infer runtime-generated fields or constructor signatures. The return type preserves a supplied base where possible, but application typing should not pretend a generated field exists statically.
Once created, dynamic construction, Mapping input, JSON, Python output, and JSON output are the same compiled operations as an equivalent static Spec. Only class definition pays the small declaration-API parsing cost.
Related tooling: public introspection¶
Frameworks can import immutable description values from talea.introspection:
from talea.introspection import inspect_spec
info = inspect_spec(Employee)
for field in info.fields:
print(field.name, field.annotation, field.required, field.alias)
FieldInfo reports the effective name, annotation, canonical schema, required
state, static default/factory, alias, Talea constraints, and normalized field
metadata. SpecInfo reports fields, Spec title/description/examples/deprecation,
generic parameters/origin/arguments, recursion and permanent-trust
classification, hook and serializer names, and supported operation names.
inspect_contract() returns ContractInfo with the original annotation,
canonical schema, metadata, and operations.
Description dataclasses, tuples, and canonical schema nodes are frozen.
Compiler source, validators, generated globals, locks, and mutable lifecycle
state are not exposed. Spec descriptions are weakly cached by class. An open
generic reports its free annotations and schema=None for fields that cannot
be canonical until specialization; its recursive and trust classifications are
therefore conservative.
This surface consumes canonical truth but is not JSON Schema. A framework can use it for command registration, routing, documentation tooling, or dependency analysis without coupling to private Talea declarations.
Related operation: immutable replacement¶
Talea implements Python 3.14's __replace__ protocol, so standard
copy.replace is the only replacement vocabulary.
from copy import replace
from talea import Spec, check
class Window(Spec):
start: int
end: int
@check("start", "end")
def ordered(start: int, end: int) -> None:
if start > end:
raise ValueError("start must not follow end")
current = Window(start=1, end=5)
updated = replace(current, end=8)
assert current.end == 5
assert updated.end == 8
Replacement keywords are canonical Python field names, never external aliases. Unknown names are rejected. Changed values run inbound transforms, structural validation, and field checks. Whole-Spec checks always rerun. Untouched permanently trusted values are reused directly; mutable current state is revalidated before publication. Validation is atomic, defaults and factories do not rerun, and unchanged mutable values remain shared by reference just as with ordinary immutable-record replacement.
The implementation does not route through to_dict() and from_mapping().
Each used Spec class compiles one smallest-path replacer lazily and owns it.
Ordinary Specs that never use replacement have no replacement artifact.
Current Python 3.14 typing preserves the concrete return type of
copy.replace(spec, ...), but the available static protocol does not validate
arbitrary replacement keyword value types as precisely as Talea's generated
constructor signature. Runtime validation remains complete.
Advanced framework recipe¶
An application can generate a normal Spec from trusted registry configuration, then publish only immutable public metadata to its tooling layer:
from talea import create_spec
from talea.introspection import inspect_spec
def build_message(name: str, configured_fields: dict[str, object]):
message_type = create_spec(name, configured_fields, module="app.messages")
description = inspect_spec(message_type)
return message_type, tuple((field.name, field.required) for field in description.fields)
Validate the configuration before calling this function. namespace is a
trusted code surface, not a safe container for user-supplied callbacks.
Presence-aware PATCH is provided by derive_spec(..., partial=True) and
apply_patch(); see Derived and PATCH
contracts. Callable-signature validation is not
implemented.
Executable dynamic lifecycle¶
This example creates a runtime account contract with a lower-bound constraint,
default, metadata, and a dynamically supplied check. It then uses Mapping/JSON
input, public introspection, validated copy.replace(), failure codes, and JSON
Schema.
"""Dynamic declarations, introspection, and validated immutable replacement."""
from copy import replace
from typing import Annotated, cast
from talea import Description, Ge, Spec, Title, ValidationError, check, create_spec
from talea.introspection import inspect_spec
@check("balance")
def practical_balance(balance: int) -> None:
if balance > 1_000_000:
raise ValueError("example account exceeds configured range")
Account = create_spec(
"Account",
{"account_id": int, "balance": Annotated[int, Ge(0)]},
defaults={"balance": 0},
namespace={"practical_balance": practical_balance},
metadata=(Title("Account"), Description("Dynamically declared account.")),
)
account: Spec = Account.from_mapping({"account_id": 7})
updated = replace(account, **{"balance": 10})
assert updated.to_dict()["balance"] == 10
assert account.to_dict()["balance"] == 0
info = inspect_spec(Account)
assert info.title == "Account"
assert [field.name for field in info.fields] == ["account_id", "balance"]
assert info.hook_names == ("practical_balance",)
assert Account.from_json('{"account_id":8,"balance":25}').to_dict()["balance"] == 25
try:
replace(updated, balance=-1)
except ValidationError as error:
assert error.errors()[0]["code"] == "greater_than_or_equal"
else:
raise AssertionError("replacement must rerun the field constraint")
try:
replace(updated, balance=1_000_001)
except ValidationError as error:
assert error.errors()[0]["code"] == "field_check"
else:
raise AssertionError("replacement must rerun the dynamic hook")
schema = Account.json_schema()
definitions = cast(dict[str, object], schema["$defs"])
account_schema = cast(dict[str, object], definitions["Account"])
properties = cast(dict[str, object], account_schema["properties"])
balance_schema = cast(dict[str, object], properties["balance"])
assert balance_schema["minimum"] == 0
Dynamic declarations should be built from trusted, low-cardinality
configuration during startup or tooling work. Exposing create_spec() directly
to request data would let remote input control declaration cardinality and
compilation work; ResourcePolicy intentionally governs data traversal, not
trusted schema creation. Prefer class syntax whenever fields are known in
source because it gives readers and type checkers the clearest contract.