Skip to content

API reference

The full public surface of the valgebra package. Every name is re-exported from the top-level valgebra namespace.

Compiling and checking

Validator

Combinators

union builtin

union(*schemas: object) -> Validator

The union of the given schemas: a value in at least one of their sets.

Parameters:

Name Type Description Default
*schemas object

The schema specs or validators to join. No argument at all is the empty union, which is nothing.

required

Returns:

Type Description
Validator

A Validator for the joined schema.

Raises:

Type Description
`NotImplementedError`

If an argument uses a form with no set (a set or tuple literal, or a typing construct that is not a type).

`ValueError`

If the joined schema crosses a size bound: depth, definitions, or nodes.

intersection builtin

intersection(*schemas: object) -> Validator

The intersection of the given schemas: a value in every one of their sets.

Parameters:

Name Type Description Default
*schemas object

The schema specs or validators to meet. No argument at all is the empty intersection, which is anything.

required

Returns:

Type Description
Validator

A Validator for the met schema.

Raises:

Type Description
`NotImplementedError`

If an argument uses a form with no set (a set or tuple literal, or a typing construct that is not a type).

`ValueError`

If the met schema crosses a size bound: depth, definitions, or nodes.

complement builtin

complement(schema: object) -> Validator

The complement of a schema: every value not in its set.

Membership is decided by the inner schema: a value belongs to the complement exactly when it is not a member of the inner. When deciding the inner raises an ordinary Python exception — a value whose comparison or __eq__ throws — that value folds to a non-member of the inner, and therefore a member of the complement. A filter of the form complement(P) over values whose own methods can raise should not rely on the complement alone to exclude them; intersect with a positive type that pins the shape instead.

Parameters:

Name Type Description Default
schema object

The schema spec or validator to complement.

required

Returns:

Type Description
Validator

A Validator for every value outside the given schema.

Raises:

Type Description
`NotImplementedError`

If schema uses a form with no set (a set or tuple literal, or a typing construct that is not a type).

`ValueError`

If the complemented schema crosses a size bound: depth, definitions, or nodes.

The whole-schema transforms open and close are methods on the compiled validator (Validator.open/close), documented above. What they move is the key-type region no clause claims: opening frees it and closing refuses it, and neither touches a region a clause already claims. A record claims none, which is why opening one admits every key; a dict[str, int] claims the str region, so opening it keeps str keys mapping to integers and frees the rest. So is simplify, which is deprecated: a schema is built in the lattice normal form, so the reduction it promised is the schema a caller already holds (the algebra guide). A fixed-length list is the native [A, B] literal (see the schema language).

Refinement markers

Regex

Annotated metadata: a string fully matches this regular expression.

Use as Annotated[str, Regex(r"[0-9a-f]{24}")]. The match is anchored — the whole string must match, as re.fullmatch does — and runs natively on the Rust path (a linear-time engine), so a pattern check stays on the validation fast path rather than crossing into Python like a predicate. A bare re.Pattern (from re.compile) is accepted as metadata too.

Immutable, because it is hashable: a marker whose pattern can be rebound after it is written into an Annotated is one whose hash changes while a schema holds it. Written out rather than taken from dataclasses, and annotated without typing, because this module is on the import path of every program that imports the package and both cost it modules -- tests/test_version.py holds the count.

Recursion

recursive builtin

recursive(
    builder: Callable[[Validator], object],
) -> Validator

Build a recursive schema as a checked fixpoint.

builder receives a placeholder validator standing for the schema being defined and returns its body. The placeholder's self-reference is resolved to a back edge, and a non-contractive body — one whose recursive reference is not under a structural constructor — is rejected.

Parameters:

Name Type Description Default
builder Callable[[Validator], object]

Called with a placeholder validator, and returns the body of the fixpoint.

required

Returns:

Type Description
Validator

A Validator for the fixpoint the body defines.

Raises:

Type Description
`TypeError`

If builder is not callable.

`NotImplementedError`

If the body uses a form with no set.

`ValueError`

If the body is not contractive -- the recursive reference sits outside every structural constructor -- or the fixpoint crosses a size bound: depth, definitions, or nodes.

Operators on a validator

A compiled validator carries an operator surface as well as its methods. These are written here rather than generated, because CPython supplies its own text for the dunder behind a type slot and that is what introspection would show.

Operator Method Meaning
obj in validator __contains__ Membership: the operator form of is_valid, so a check reads as the set test it is.
a \| b __or__, __ror__ The union of the two schemas. \| is the operator typing already uses for a union; intersection and complement have no typing operator and stay named calls. The reflected form is what makes None \| validator work.
a == b __eq__ Equality of the normal form: the schema trees, recursive definitions, and pooled constants all match, after the lattice laws construction settles — so a difference of member order, of a repeat, or of an identity is not one. Ask is_equivalent for the semantic question — whether two schemas denote the same set, which needs a containment rather than a law.
hash(validator) __hash__ Consistent with ==, so a validator is a dict key or a set member. It digests the schema shape and definitions only, never the pooled constants, so an unhashable constant cannot break it.
repr(validator) __repr__ A rendering of the schema as an expression that builds it.

copy.copy and copy.deepcopy both return an equivalent validator; a validator is immutable, so the copy shares the pool rather than duplicating it.

repr is a rendering, not a serialization. What it gives back is an expression that builds the same schema — a recursive schema as the recursive call it is, an open record as the catch-all entry it carries, the nullary product as tuple[()] — so it can be pasted into a session and read back. Five things it cannot render as an expression, and none of them reads back quietly: a class, which is an object rather than syntax and appears as its name; a predicate, which is a function and appears as Predicate(...), and which the frontend refuses where it is built; a constant too long to print, which is cut mid-string and is a syntax error where it is parsed; a meet of two classes that each declare attributes, which flattens to the two classes and their two attribute records, and a record standing apart from its class prints as object(x=int), a form no constructor spells -- the schema does not record which class each record came from, so the render cannot fold them back; and a schema past the renderer's own depth bound, which gives up and prints <...>. The bound is within reach: no single annotation can be written deep enough, since the frontend refuses past MAX_SCHEMA_DEPTH and the renderer's bound sits above it, but a chain of recursive definitions composes — the render descends into each in turn, so a hundred shallow links reach a depth one annotation cannot. The mark is deliberately not an ellipsis: ... reads as a schema inside a subscript, so a truncated render would parse and hand back a different validator with nothing to say it had been cut. Do not parse a repr: it is for a person to read, and inspection says how to ask a schema questions instead.

Lattice bounds

anything and nothing are the two bounds of the lattice, and both are Validator instances rather than schema forms you construct.

  • anything — the top: every Python value is a member, so anything.is_valid(x) is True for every x. It is the identity of intersection and the absorbing element of union.
  • nothing — the bottom: no value is a member, and nothing.is_empty() is True. It is the identity of union and the absorbing element of intersection.

nothing is also where the third answer about emptiness is asked for: is_empty is a predicate and reports the proof, while relation_to(nothing) reports which of proof, refutation and decline the procedure reached (the boundary).

Both are ordinary validators: they compose with the combinators, compare with is_subtype_of, and are what the constructors fold to — intersection(int, complement(int)) is nothing (the algebra). Any is the same set and the same schema as anything, differing only in what repr gives back; see Any versus anything.

from valgebra import Validator, anything, complement, intersection, nothing, union

assert anything.is_valid(object())
assert nothing.is_empty()
assert Validator(int).is_subtype_of(anything)
assert nothing.is_subtype_of(int)
assert intersection(int, anything).is_equivalent(int)
assert union(int, nothing).is_equivalent(int)
assert complement(anything).is_equivalent(nothing)
assert Validator(int).relation_to(nothing) == "not_subset"  # a value says so

What raises, and when

A failure at build time — when the schema is compiled — is a different kind of event from a failure at validation time, and it raises a different exception. ValidationError is only ever the second. Building raises one of three, and which one says what went wrong:

Raised When Example
NotImplementedError The spec names a form with no decidable runtime membership. Sequence[int], Mapping[str, int], a TypeVar, Final, ClassVar
NotImplementedError Compiling descends one level past the construction depth bound without reaching a leaf. a self-referential class, whose field type names the class
ValueError A constructed schema crosses a size bound: depth, definitions, or nodes. growing a schema in a loop with \|, union, intersection, open
ValueError A marker's value cannot denote a set. MultipleOf(0)
ValueError A recursive body is not contractive — its back edge is not under a structural constructor. recursive(lambda s: s)
TypeError An argument is the wrong Python type for the call. validate_json(123), load(123)

The two NotImplementedError rows are different bounds that happen to share a class. The first is about the form and no depth would help it; the second is about depth while compiling, and is what a self-referential class reaches because its field type names the class again — the frontend descends one level past what a constructed schema may carry, so the message names the bound and the schema past it is refused rather than half-built. The ValueError depth row is that construction bound itself, on the schema a sequence of calls has built, and MAX_SCHEMA_DEPTH is importable so code that sizes a schema reads it rather than repeating it. The resource limits guide covers all three sizes.

from collections.abc import Sequence
from dataclasses import dataclass

from valgebra import Validator, recursive


@dataclass
class Node:
    next: "Node"


for spec in (Sequence[int], Node):
    try:
        Validator(spec)
        raise AssertionError("expected a rejection")
    except NotImplementedError:
        pass

try:
    Validator(recursive(lambda schema: schema))
    raise AssertionError("expected a rejection")
except ValueError:
    pass

try:
    Validator(int).validate_json(123)
    raise AssertionError("expected a rejection")
except TypeError:
    pass

Every one of these is raised before any value is checked, so a schema that compiles is a schema whose every membership question is answerable.

Errors

ValidationError

Raised by validate, validate_json, load and ensure when a value is not a member of the schema's set. It subclasses Exception, and carries a structured, machine-readable model as well as a message. The attributes are set on the instance, so they are written here rather than generated.

Attribute Type Meaning
errors tuple[dict[str, object], ...] One item per independent failure, each a JSON-serializable dict with the keys code, path, message, expected and value. json.dumps(err.errors) is the JSON form of the whole report. One call reports every failure unless fail_fast=True stops the walk at the first.
code str The first item's stable failure code, such as int_type, missing_key or literal_error.
path tuple[str \| int, ...] The first item's location from the root, string keys and integer indices, empty at the root.
message str The first item's rendered one-line message. str(err) summarizes every failure rather than only this one.
expected str A short label of the set the first item expected, such as int.
value str A repr-style summary of the first item's offending value.

The error model guide gives the path format and the properties a code carries; the codes themselves are enumerated by tests/test_error_codes.py, which asserts each against the node kind that emits it, because a list written out beside the walk drifts from it.

import json

from valgebra import ValidationError, Validator

try:
    Validator({"a": int, "b": str}).validate({"a": "x", "b": 1})
except ValidationError as err:
    assert err.code == "int_type"
    assert err.path == ("a",)
    assert [item["path"] for item in err.errors] == [("a",), ("b",)]
    assert json.dumps(err.errors)  # the whole report is JSON-serializable

Construction limits

Three module constants give the bounds every schema-growing call is checked against, so a caller sizing a schema reads the number rather than repeating it. Resource limits says what each one bounds and why.

name value bounds
MAX_SCHEMA_DEPTH 128 levels of structural nesting
MAX_DEFINITIONS 128 recursive definitions in one schema
MAX_SCHEMA_NODES 100,000 total schema nodes
from valgebra import MAX_SCHEMA_DEPTH, Validator, complement

schema = Validator(int)
for _ in range(MAX_SCHEMA_DEPTH - 1):
    schema = complement(schema)
# An odd number of complements is the complement of `int`, so `1` is outside.
assert schema.is_valid(1) is ((MAX_SCHEMA_DEPTH - 1) % 2 == 0)

Package version

valgebra.__version__ is the distribution version as a string. It comes from the Cargo workspace manifest, which is what maturin derives the wheel's metadata from — so it matches the built wheel and never drifts from a hand-maintained literal. Taken from the compiled extension rather than read back out of the installed metadata, because the metadata reader costs twenty milliseconds of import time for a string the manifest already carries.

import valgebra

assert isinstance(valgebra.__version__, str)
assert valgebra.__version__

What is public

Every name above is public and is reached from the top-level valgebra namespace. valgebra.__all__ lists the schema surface — Validator, the combinators, Regex, recursive, the two lattice bounds, the three construction limits, and ValidationError; __version__ is public too and is not in it, being metadata rather than part of the algebra.

The compiled extension underneath, valgebra._valgebra, is private: its layout, its module name, and which names it carries are free to change in any release. Import from valgebra.