Schemas from TypedDicts
A TypedDict describes the shape of a plain dict: which keys it has and what
each value is typed as. TypedDictSchema reads that shape and builds a schema
from it, so you validate against the keys you already declared instead of writing
the schema twice.
The annotation engine is shared with dataclasses, but the two are different tools for different jobs. A dataclass schema validates a mapping and hands you back a constructed instance. A TypedDict is a plain dict at runtime, so a TypedDict schema validates the mapping and returns it unchanged, just typed. Dict in, dict out, at no construction cost.
The basics
Section titled “The basics”Pass a TypedDict type to TypedDictSchema. The validated mapping comes back as
is, and your type checker sees it as the TypedDict, so result["port"] is an
int to the type checker with nothing extra at runtime.
from typing import TypedDict
from probatio import TypedDictSchema
class Config(TypedDict): port: int host: str
schema = TypedDictSchema(Config)schema({"port": 8080, "host": "nas"}) # {'port': 8080, 'host': 'nas'}By default every key is required, and a missing one is reported like any other validation error:
from typing import TypedDict
from probatio import TypedDictSchema
class Config(TypedDict): port: int host: str
TypedDictSchema(Config)({"port": 8080}) # required key not provided @ data['host']The keys are closed: an unknown key is rejected, the same as the default dict
schema. Pass extra=ALLOW_EXTRA (keyword-only) to keep unknown keys instead.
Required and optional keys
Section titled “Required and optional keys”A TypedDict carries its own notion of which keys are required, and the schema
honors it. A total=False class makes every key optional; Required and
NotRequired set it per key. An optional key that is absent is simply left out of
the result, no default is invented.
from typing import TypedDict, NotRequired
from probatio import TypedDictSchema
class Server(TypedDict): name: str port: NotRequired[int]
schema = TypedDictSchema(Server)schema({"name": "nas", "port": 22}) # {'name': 'nas', 'port': 22}schema({"name": "nas"}) # {'name': 'nas'}A total=False class flips the default, so nothing is required:
from typing import TypedDict
from probatio import TypedDictSchema
class Partial(TypedDict, total=False): a: int b: str
TypedDictSchema(Partial)({}) # {}Annotations drive the validators
Section titled “Annotations drive the validators”Each field’s annotation becomes a validator. The mapping is deep, not just the
container type: a parameterized generic keeps its element types, a union with
None becomes Maybe, and a nested TypedDict (or dataclass) recurses into its
own schema. The full table is the same one
dataclasses use.
from typing import TypedDict
from probatio import TypedDictSchema
class Config(TypedDict): port: int host: str
class Service(TypedDict): name: str config: Config
TypedDictSchema(Service)({"name": "web", "config": {"port": 80, "host": "nas"}})# {'name': 'web', 'config': {'port': 80, 'host': 'nas'}}For anything beyond the type check (a length, a range, a pattern), the same two
options as dataclasses apply: pass additional_constraints, a map from key to a
validator that runs after the type check, or put the rule on the field with
Annotated.
from typing import TypedDict
from probatio import TypedDictSchema, Length
class Config(TypedDict): port: int host: str
schema = TypedDictSchema(Config, {"host": Length(min=2)})schema({"port": 8080, "host": "nas"}) # {'port': 8080, 'host': 'nas'}The functional form
Section titled “The functional form”create_typeddict_schema(typeddict_type, additional_constraints=None) builds the
same schema without the class wrapper. It returns a plain Schema, so it does not
carry the TypedDict type for the type checker the way TypedDictSchema does.
from typing import TypedDict
from probatio import create_typeddict_schema
class Config(TypedDict): port: int host: str
create_typeddict_schema(Config)({"port": 8080, "host": "nas"})# {'port': 8080, 'host': 'nas'}What carries over from dataclasses
Section titled “What carries over from dataclasses”The two share the annotation engine, so the dataclasses
guide covers the rest, and it all behaves the same with a
TypedDict:
- The annotation mapping table.
- Coercing a type wherever it appears
with
register_type/type_registry. - Recursive and mutually recursive types, with the same depth guard.
- Discriminated unions over a shared literal tag.
The one difference is the result: a dataclass schema constructs an instance, a TypedDict schema returns the validated dict.
Limits
Section titled “Limits”The value is not coerced between container types: a list stays a list even where
the annotation says tuple.