pyproject.toml · v1.2.3

version
1.2.3
python
>=3.11
license
MIT
dependencies 0
none
extra [dev] 3
  • mypy
  • pytest
  • hypothesis
installs 0
nothing

# pytypehint

Define data once with ordinary Python type hints and dataclasses. pytypehint compiles them into strict schemas that validate input with exact types and no implicit coercion, build nested dataclasses, and export a portable description another process can read.

Use @immutable for deeply immutable models that validate themselves and reuse already validated children. pytypehint is the schema and validation core of func-to-web.

from dataclasses import dataclass
from typing import Annotated
from pytypehint import Min, struct_of

@dataclass
class Item:
    name: str
    quantity: Annotated[int, Min(1)] = 1

schema = struct_of(Item)
item = schema.build({"name": "Pen"})          # Item(name='Pen', quantity=1)
schema.build({"name": "Pen", "quantity": 0})  # SchemaValueError: quantity

pytypehint turns a dataclass or a function signature into a compiled schema: struct_of(Item) returns a Struct, signature_of(fn) a Signature. Compile once and reuse. Validation is exact: float rejects int, int rejects bool, and nothing is coerced. The first error raises SchemaTypeError or SchemaValueError with the failing field or index in its path.

# Immutable models

Declare constraints once. Normal construction and dataclasses.replace validate automatically; nested instances keep their identity.

from dataclasses import replace
from typing import Annotated
from pytypehint import Max, Min, immutable

Positive = Annotated[int, Min(1)]
Channel = Annotated[float, Min(0.0), Max(1.0)]

@immutable
class Exposure:
    stops: float = 0.0

@immutable
class Grayscale:
    pass

@immutable
class Layer:
    name: Annotated[str, Min(1)]
    effect: Exposure | Grayscale
    color: tuple[Channel, Channel, Channel, Channel] = (0.0, 0.0, 0.0, 1.0)

@immutable
class Document:
    size: tuple[Positive, Positive]
    layers: tuple[Layer, ...] = ()

photo = Layer(name="Photo", effect=Exposure(stops=1.5))
original = Document(size=(1920, 1080), layers=(photo,))
resized = replace(original, size=(3840, 2160))

assert resized.layers[0] is photo
assert original.size == (1920, 1080)

The new document checks its size and layer references, without revisiting the existing layer's fields. Tuples are checked; existing immutable children are reused.

replace(photo, color=(1.0, 0.0, 0.0, 2.0))  # SchemaValueError, path: ('color', 3)
Exposure(stops="1.5")                       # SchemaTypeError
original.size = (800, 600)                  # FrozenInstanceError

Fields are restricted to immutable scalars, tuples and other @immutable models; see Immutable models for the full contract, inheritance and defaults.

# Portable data and inspection

The same models work with the schema API. decode restores portable values such as JSON arrays and discriminated unions, and to_dict describes the schema for a UI or another process:

from pytypehint import struct_of

schema = struct_of(Document)
loaded = schema.build(schema.decode({
    "size": [1920, 1080],
    "layers": [{
        "name": "Photo",
        "effect": {"$type": "Exposure", "stops": 1.5},
    }],
}))

assert loaded == original
contract = schema.to_dict()  # Portable description for a UI or another process

Type constraints and interface metadata live on the same field:

from pytypehint import Label, Slider, Step

@immutable
class Brush:
    size: Annotated[int, Min(1), Max(256), Label("Brush size"), Step(1), Slider()] = 32

size_field, = struct_of(Brush).fields
size_shape, = size_field.shape
assert size_field.label.value == "Brush size"
assert size_shape.max.value == 256

# The four operations

Method Result
build(data) Dataclass instance; signature_of(fn) returns kwargs for fn(**kwargs)
resolve(data) Validated dictionary with missing defaults filled at this level
decode(data) Portable values restored to Python types
to_dict() Portable schema description