Skip to content

The validation model

A schema is data that describes data. You write it with plain Python, Probatio compiles it once into a fast validator, and then you call that validator with a value. This page is the mental model the rest of the guides build on.

You do not call a builder API to construct a schema. You hand Schema an ordinary Python object, and its shape is the rule:

from probatio import Schema
Schema(int)(42) # 42
Schema("on")("on") # 'on'
Schema([int])([1, 2, 3]) # [1, 2, 3]
Schema({"a": int})({"a": 1}) # {'a': 1}

Each kind of object means something:

You writeIt means
a type (int)the value must be an instance of that type
a literal ("on")the value must equal that literal
a dicta mapping, each key and value validated by its own schema
a list/tuple/seta sequence, each item matching one of the element schemas
a callablecalled with the value; it returns the result or raises
a nested Schemavalidated as its own schema

Because a schema is just data, schemas compose: a dict can hold lists of nested dicts, and any value position can be a validator like All or Coerce.

Schema(...) does its analysis up front. Building the schema is the expensive step; calling it is cheap, so build a schema once and reuse it:

from probatio import Schema, Required
schema = Schema({Required("name"): str}) # compiled here
for record in [{"name": "a"}, {"name": "b"}]:
schema(record) # cheap, repeated calls

Keep schemas at module scope, or build them once in a constructor, rather than rebuilding inside a hot loop.

A validator returns the validated result; it does not mutate the input. The result can differ from the input, because validators may normalize: Coerce converts types, defaults fill in absent keys, and string transforms rewrite the value.

from probatio import Schema, Coerce
schema = Schema({"port": Coerce(int)})
data = {"port": "443"}
schema(data) # {'port': 443}
data # {'port': '443'} (unchanged)

Failure is an exception, not a return value

Section titled “Failure is an exception, not a return value”

A valid value comes back from the call. An invalid one raises, so there is no “is it valid” flag to check; you validate inside a try, or let the error propagate.

from probatio import Schema, Invalid
schema = Schema(int)
try:
schema("not a number")
except Invalid as err:
print(err) # expected int

Everything Probatio raises for bad data is an Invalid (or a MultipleInvalid collecting several). A broken schema definition, by contrast, raises SchemaError, because that is a programming mistake, not bad input. See error handling for the full picture.

Within All, validators run left to right and each receives the previous result, so All(Coerce(int), Range(min=0)) coerces before it range-checks. The reverse order would range-check a string. Marker defaults and group rules also have defined timing. Where order is significant, the guides call it out, and it matches voluptuous.