Compiled schemas
Probatio validates with one interpreted engine. On top of that, it can compile a schema into a specialized validator: a flat function generated for that exact schema, with the common per-key work unrolled and the common validators inlined. It is faster, and it is the same result: on a representative config schema, compiling takes a validation from roughly 2 µs to under 1 µs, about seven times faster than the same schema in voluptuous (see Numbers). Compiling only changes speed, never what a schema accepts, rejects, or how it reports an error.
By default this happens on its own. You do not have to do anything to get it, and in most cases you should not have to think about it at all. This page explains what it does, how to steer it when you want to, and the one case where you should turn it off.
The default: it compiles itself
Section titled “The default: it compiles itself”The process starts on the AUTO policy. Under AUTO, a schema validates
interpreted and quietly counts how often it is called. Once it has proven hot (it
has been validated enough times to be worth the cost), it compiles itself and runs
through the generated validator from then on. The line is currently drawn at
roughly 50 validations, an implementation detail that may move. A schema you build
and call once, or a handful of times, never crosses it, so it stays interpreted
and never pays for code generation.
There is no public way to ask whether a schema has compiled; the observable effect is throughput, so measure that instead. Performance shows how to benchmark it yourself.
So the practical rule is the same as it has always been: build a schema once, at module scope, and reuse it. The hot ones speed themselves up.
from probatio import Schema, All, Coerce, Range
# Built once, reused for every value. After enough calls it compiles itself.PORT = Schema(All(Coerce(int), Range(min=1, max=65535)))
PORT("443") # 443PORT(8080) # 8080Asking for it explicitly
Section titled “Asking for it explicitly”When you know a schema is hot and you do not want to wait for the warmup, opt in per
schema. The compile flag and Schema.compile() both do that; compile() is
spelled to mirror re.compile, and unlike the flag it generates right away rather
than on first use.
from probatio import Schema, Required, Optional
# Opt in at construction (generated on first validation).schema = Schema({Required("name"): str, Optional("age"): int}, compile=True)
# Or opt in eagerly, generating now. Returns the same schema.schema = Schema({Required("name"): str, Optional("age"): int}).compile()DataclassSchema and TypedDictSchema take the same flag, and they are the
clearest win: compiling fuses field validation and object construction into one
function with no intermediate dict.
from dataclasses import dataclassfrom probatio import DataclassSchema
@dataclassclass Point: x: int y: int
POINT = DataclassSchema(Point).compile()POINT({"x": 1, "y": 2}) # Point(x=1, y=2)To opt a schema out, pass compile=False. It then stays interpreted whatever the
process policy is.
The process-wide policy
Section titled “The process-wide policy”The default for any schema that does not set its own flag is the process policy. Set it once, early, from deliberate startup code. There is no environment variable on purpose: this is an architectural choice, not a deployment toggle that flips behavior invisibly.
from probatio import CompilePolicy, set_compile_policy
set_compile_policy(CompilePolicy.OFF) # never compile unless a schema opts inset_compile_policy(CompilePolicy.ON) # compile every eligible schema on first useset_compile_policy(CompilePolicy.AUTO) # the default: compile a schema once it is hotA per-schema compile flag always wins over the policy, in either direction. The
order of precedence is: schema.compile(), then the compile=True/compile=False
flag, then the process policy.
What compiles, and what does not
Section titled “What compiles, and what does not”The generator handles the common shapes: dict (mapping) schemas, dataclasses, and
TypedDicts, including nested mappings, and single-element list schemas ([str],
[All(Coerce(int), Range(...))]) both inside a mapping and at the top level. It
inlines the validators that show up most: Coerce to int/float, numeric
Range, plain In membership, type checks, an Any of literal choices
(Any("on", "off"), validated as a membership test), Maybe(X) (an optional
validated value), and All/Any chains of those.
Anything it does not handle is simply left interpreted. A recursive schema (Self
or a recursive JSON Schema $ref), an exclusive or inclusive group, a multi-element
list (an “each item matches one of these” union), a list whose element does not
inline, an exotic key: the schema works exactly as before, just without the speedup.
You never lose correctness by compiling; at worst you do not gain speed.
This is the heart of the design. The generated function is a fast success path only. The moment anything is off (a missing required key, a type mismatch, a validator that raises, an unexpected key) it hands the value back to the interpreted engine, which produces every error, path, code, and ordering. So there is one set of validation semantics, not two, and the compiled path differs only by speed (with one caveat for side-effecting validators on a failing validation, noted below).
The one time to turn it off
Section titled “The one time to turn it off”Each compiled schema holds a generated code object and a small namespace capturing its validators. For a fixed set of schemas (the normal case, even a large one built once at import) that is bounded and cheap, and schemas of the same shape share a single code object behind the scenes.
The exception is a workload that builds an unbounded stream of unique schemas,
a new distinct schema per request that is never reused. There, compiling each one
grows memory without bound. AUTO already protects you (a one-shot schema never
crosses the warmup threshold, so it is never compiled), but if you run such a
workload under ON, turn it off:
from probatio import CompilePolicy, set_compile_policy
set_compile_policy(CompilePolicy.OFF)If your schemas are a fixed set built once, which is almost always the case, you do not need to think about this.
Numbers
Section titled “Numbers”On the bundled benchmarks, compiled validation runs up to roughly seven times faster than the same schema in voluptuous, and the interpreted engine already runs about one and a half to three and a half times faster. The exact ratio moves with the machine, the Python version, and the schema. See Performance for the cost model, the honest caveats, and how to measure it yourself.