typing_validation.composition

Building a validation function specialised to one fixed type, by composing a closure per node of the interned graph.

Construction is cheap and structurally shared: nodes are interned on the type itself, so list[int] is analysed once and its closure reused everywhere it occurs — inside dict[str, list[int]], inside tuple[list[int], ...], and inside anything else mentioning it. The result is a graph over distinct sub-types, not a tree over syntactic occurrences.

The shape here is chosen by measurement, and it is not the obvious one. Composing closures that simply call one another is the obvious one, and it is 3x faster than the interpreter per node — but it recurses once per level of the value, so it raises RecursionError on exactly the deeply nested values the interpreter goes out of its way to handle. Two mechanisms disagreeing about one value, one of them by crashing, is what the architecture exists to prevent. Composing closures that all push onto a shared work stack fixes that, and is only 1.16x faster, which does not earn a second mechanism at all.

Depth grows only where a check can descend. A check that answers from the value alone cannot grow the stack, so calling it directly costs one call and risks nothing. So a container calls the children that cannot descend and pushes the ones that can. On list[int] that is the fast shape, at 2.9x the interpreter; on a recursive alias it is the safe one.

Check

Check = Check

One node’s composed check: given a value, the work stack and the union bookkeeping, whether the value is valid here, having pushed whatever remains.

Every check takes all three whether it uses them or not. A check that cannot descend ignores both, which costs two unused arguments and saves the wrapper that dropping them would need.

Composed

Composed = Composed

A check, and whether it can push.

The second is what decides call-versus-push at every parent, and it is a property of the check rather than of the node’s children: a union of plain classes has children and still collapses to a single isinstance, so it can no more descend than int can.

Runner

Runner = Runner

A composed check with its driver attached, as a plain predicate.

Named rather than written inline because the documentation pipeline cannot parse a Callable parameter list — see CLAUDE.md — and because naming it says what it is.

Stack

Stack = Stack

Type alias.

Type aliases are created through the type statement:

type Alias = int

In this example, Alias and int will be treated equivalently by static type checkers.

At runtime, Alias is an instance of TypeAliasType. The __name__ attribute holds the name of the type alias. The value of the type alias is stored in the __value__ attribute. It is evaluated lazily, so the value is computed only if the attribute is accessed.

Type aliases can also be generic:

type ListOrSet[T] = list[T] | set[T]

In this case, the type parameters of the alias are stored in the __type_params__ attribute.

See PEP 695 for more information.

Unions

Unions = Unions

Type alias.

Type aliases are created through the type statement:

type Alias = int

In this example, Alias and int will be treated equivalently by static type checkers.

At runtime, Alias is an instance of TypeAliasType. The __name__ attribute holds the name of the type alias. The value of the type alias is stored in the __value__ attribute. It is evaluated lazily, so the value is computed only if the attribute is accessed.

Type aliases can also be generic:

type ListOrSet[T] = list[T] | set[T]

In this case, the type parameters of the alias are stored in the __type_params__ attribute.

See PEP 695 for more information.

Validator

Validator = Validator

What validator returns: the same contract as validate, minus the type, which it already knows.

runner_for

runner_for(node, /)[source]

A node’s composed check as a plain predicate, driver and all.

What a caller outside this module needs, and the only safe way to ask: a check that can descend pushes onto a stack that something must then drain, and handing it a throwaway list silently loses the work — which reports valid for a value that is not.

Parameters:

node (TypeNode)

Return type:

Runner

validator

validator(t, /)[source]

A validation function specialised to one type.

Same contract as validate, and the same verdict on every value. It pays for itself when the same type is validated repeatedly, because it analyses the type once rather than once per value:

check = validator(list[int])
for payload in payloads:
    check(payload)

Unlike validate, this refuses an unsupported type immediately rather than when a value happens to reach the unsupported part. It has to: it analyses the whole type before it sees any value. So validator(list[Callable[[int], int]]) raises here, where validate([], list[Callable[[int], int]]) returns True.

Raises:

UnsupportedTypeError – if the type, or any component of it, is not one this library can validate against.

Parameters:

t (Any)

Return type:

Validator