Skip to content

Performance

Probatio is pure Python with no native extension. That is a deliberate choice (see ADR-001 and ADR-004): the drop-in promise and easy installation matter more than squeezing out the last cycle. This page explains the cost model and how to measure it yourself, with honest numbers.

A Schema compiles its definition once, when you construct it. After that, calling it is the cheap part. So the rule is simple: build the schema once, then reuse it.

from probatio import Schema, All, Coerce, Range
# Built once. Reused for every value.
PORT = Schema(All(Coerce(int), Range(min=1, max=65535)))
PORT("443") # 443
PORT(8080) # 8080

The expensive work (walking the definition, resolving markers, wiring up the validators) happens in the constructor. If you rebuild the schema on every call, you pay that cost every time for nothing.

On the bundled benchmark scenarios, warm validation in Probatio runs roughly twice as fast as the same schema in voluptuous. Roughly. The exact ratio moves with the machine, the Python version, and the shape of the schema, so treat “about 2x” as a ballpark, not a promise.

The tracked benchmarks are the CodSpeed ones in bench/test_benchmarks.py. Those run per pull request, so a performance regression shows up in review.

The rough comparison against voluptuous:

Terminal window
just bench

That prints a small table of total time and the Probatio/voluptuous ratio per scenario. Run it a few times; the absolute numbers wander.

The tracked CodSpeed benchmarks (walltime locally, tracked in CI):

Terminal window
just codspeed

These are not part of the normal test run. They exercise the hot paths: validating a config-style mapping, compiling a schema from scratch, validating a list of coerced numbers, the deepest-error path through a failing Any, and an exclusive-group post-pass.

  • Build each schema once, at module scope, and reuse it. This is the single biggest win.
  • Never rebuild a schema inside a hot loop. Compilation is the expensive part.
  • Reuse nested schemas too. A Schema you reference from another schema is compiled once and shared.
  • Remember it is pure Python by design. There is no native extension to build or install, and no compiled fast path to fall back to. If you need raw throughput beyond what this gives you, that is a trade-off you are choosing knowingly.

A compiled Schema is meant to be built once and called many times, and that is also what makes it safe to share. Validation does not mutate the schema: it reads the compiled checks and returns a new value, so the same Schema instance can be called from multiple threads at once. Build it once at import time and let every worker use it.

The one caveat is your own code. If you write a custom validator that keeps mutable state, or pass a default factory that is not safe to call concurrently, that state is yours to make thread-safe. Probatio’s own validators hold no per-call state.