Comparison to alternatives
Probatio does runtime validation of arbitrary Python data. The schema is data:
plain types, dicts, lists, and callables. You build a schema from those pieces,
call it with a value, and get back the normalized value or an Invalid error
with a path to what went wrong. There are no model classes to declare. It is a
drop-in reimplementation of voluptuous,
and it is pure Python with no native extension.
That shapes where it fits. Probatio is at home when your data is free-form dicts
and lists (config files, request bodies, payloads from another system) and you
want the schema to stay data you can build, pass around, and compose at runtime.
It can also hand back a typed object: point DataclassSchema or
TypedDictSchema at a dataclass or TypedDict you already own, and you get a
validated, statically typed result. What it is not is a model framework: no
base class your types inherit, and validation is all it does, with no bundled
serialization or settings. For that, other tools do a better job, and this page
says where.
The short answer
Section titled “The short answer”| Pick this when | Reach for |
|---|---|
| You use voluptuous today and want a maintained replacement | Probatio |
| Your data is plain dicts and lists, schema as data | Probatio |
| You want validated, typed results from your own dataclasses or TypedDicts | Probatio |
| You want a model framework: one class for validation, serialization, settings | pydantic |
| You want declarative schema classes with load and dump | marshmallow |
| Your schemas are cerberus rule dicts | cerberus, or port to Probatio |
| Your contract is a JSON Schema document | jsonschema |
| You are (de)serializing your own classes | attrs / cattrs |
| You (de)serialize dataclasses fast, in a known format | mashumaro |
| You just need a dict turned into a dataclass, no validation | dacite |
voluptuous
Section titled “voluptuous”This is the library Probatio replaces. Same model, same public API: a schema is data, and you validate by calling it. Probatio is the clean-room reimplementation that is actively maintained and MIT licensed. No code was copied from voluptuous; behavior is matched, not source.
If you already run voluptuous, the switch is usually a one-line import change. Read migrating from voluptuous for what carries over and the few intentional differences.
from probatio import Schema, Required, Optional
schema = Schema({Required("name"): str, Optional("age"): int})schema({"name": "Frenck", "age": 40})# {'name': 'Frenck', 'age': 40}pydantic
Section titled “pydantic”Pydantic models your data as classes built on Python type hints, with a native
(Rust) core and strong editor and type-checker support. You declare a model class
that inherits from pydantic’s BaseModel, and that one class is your schema, your
type, your validator, and your serializer: typed objects back, with
autocompletion, static checking, coercion, and .model_dump() all following from
the hints.
Probatio hands back typed objects too, which is newer than its reputation
suggests: DataclassSchema and TypedDictSchema validate into a dataclass or
TypedDict you already own, and both are generic, so the result is statically
typed (a Config, not Any), editor and checker included. The difference is the
framework. Pydantic asks your types to be pydantic types, the BaseModel base
class, and bundles serialization and settings on top; Probatio validates
free-form data, or validates into the plain types you already have, and does
validation only, you own the serialization. So it is the better fit when your
data is plain dicts and lists, when the schema should be data you build and
compose at runtime rather than a class you declare, when you want to validate into
your own dataclasses without adopting a model framework, or when you are replacing
voluptuous.
On raw speed the Rust core is genuinely fast, but the advantage is not uniform, and it is worth being precise about where it comes from. It is largest on heavy coercion and decoding: parsing a stack of ISO datetimes and enums out of JSON, where the native code does real work Probatio does in Python. The cross-library benchmark on the performance page is exactly that kind of workload, and pydantic v2 sits near the top of it. The advantage shrinks on a schema dominated by your own custom validators, because a native core cannot run a Python validator natively; it has to call it across the Rust boundary (pydantic does so efficiently, but it is still a Python call), so the speedup is muted exactly where the work is your code rather than the library’s. “Faster because Rust” holds in the parsing-heavy regime, not everywhere, and Probatio stays pure Python with no native extension to build or install.
marshmallow
Section titled “marshmallow”marshmallow validates and (de)serializes through declarative schema classes: you
declare a Schema subclass with typed fields, and load/dump turn dicts into
validated data (or objects, via @post_load) and back. It validates, like Probatio
and pydantic, but the schema is a class you declare with explicit field objects, and
it is built around the load and dump cycle, with a long-standing ecosystem (Flask,
webargs, and the like).
Probatio is the better fit when the schema is data you build and compose at runtime rather than a class you declare, when you validate free-form dicts, or when you are replacing voluptuous. marshmallow fits when you want explicit schema classes and its serialization story. Probatio is also a good deal faster on the cross-library benchmark (marshmallow does more per field and is pure Python), but speed is not the main axis here; the model is.
cerberus
Section titled “cerberus”cerberus is the other schema-is-data validator. You describe rules in a dict
of string rule names ({"name": {"type": "string", "required": True}}), hand
it to a Validator, and check the result. The shared instinct with Probatio is
real: the schema is data, not a class hierarchy.
The difference is what that data is made of. A cerberus schema is a small rule language interpreted by the library, so extending it means registering a rule or subclassing the validator. A Probatio schema is live Python objects: plain types, callables, and validators you compose directly, so a custom rule is just a function, and the schema participates in the rest of your code (imports, refactoring, composition) like any other Python value. cerberus is maintained and still ships releases; if its rule dicts fit how you think, it is a fine choice. If you want the schema itself to be Python you compose, Probatio fits better.
jsonschema
Section titled “jsonschema”The jsonschema library validates data against the
JSON Schema specification. If your contract already
is a JSON Schema document, perhaps shared across services or languages, then a
JSON Schema validator is the honest fit. That is its job, and it does it.
Probatio is not a JSON Schema validator. It does interoperate, though: it can read a JSON Schema into a Probatio schema and write one back out, for the constructs that map cleanly between the two.
attrs and cattrs
Section titled “attrs and cattrs”attrs defines classes with less boilerplate, and cattrs structures and
unstructures those classes to and from plain data. That is class
(de)serialization: turning a dict into a typed instance and back.
That is a different job from schema-style validation of free-form data. If your goal is to (de)serialize your own classes, attrs and cattrs fit. If your goal is to validate arbitrary incoming data against a schema you describe as data, Probatio fits. They can also pair up: validate the raw data with Probatio, then structure the result into your classes with cattrs.
mashumaro
Section titled “mashumaro”mashumaro (de)serializes dataclasses to and from dicts and formats like JSON,
YAML, and msgpack. It code-generates the conversion per class, so it is fast, and
it reads the field types from the dataclass itself. Home Assistant leans on it for
exactly that: turning stored or transmitted payloads into typed dataclass
instances at speed.
Like attrs and cattrs, that is class (de)serialization driven by your declared types, not schema validation of free-form data. Reach for mashumaro when you own the dataclasses and want them filled from a known format quickly. Reach for Probatio when the data is arbitrary and the schema is the thing you describe and compose at runtime. They pair the same way: validate the incoming data with Probatio, then hand the result to mashumaro to build your dataclasses.
The two are close on the dict-to-dataclass path, which is worth seeing precisely
because they do different amounts of work. mashumaro deserializes and largely
trusts the declared types; Probatio’s DataclassSchema validates every field
against its type and then constructs. On a small dataclass, mashumaro builds it in
about 0.5 µs and Probatio in about 0.7 µs compiled (about 1.5 µs interpreted,
before the schema compiles itself), so compiled Probatio is within roughly 1.4x
while actually validating, and the gap all but closes as the field count grows. That is the trade: mashumaro is faster because it trusts the input,
Probatio costs a little more because it does not. If the input is wrong (a string
where an int belongs), mashumaro may hand you a dataclass whose values do not match
their annotations, and Probatio rejects it. Numbers, and how to run them yourself,
are on the Performance page (just bench-dataclass).
When the input is trusted (your own data round-tripping back, or already validated
upstream), Probatio’s opt-in
DataclassSchema.construct
builds without validating, and on that path it is the fastest in the cross-library
benchmark, ahead of mashumaro and cattrs, because it generates a flat constructor per
dataclass. The one thing it does not do is decode: a datetime field keeps whatever
the dict held, where mashumaro converts a string to a datetime. So for data that
needs decoding, mashumaro is still the right reach; for already-typed trusted data,
construct wins.
dacite and dataclasses-json
Section titled “dacite and dataclasses-json”Both turn a dict into a dataclass. dacite’s from_dict builds an instance from a
dict with light type checking and no schema language; dataclasses-json adds
from_dict/to_dict (and JSON) to a dataclass through a decorator or mixin. They are
the same family as cattrs and mashumaro: dataclass (de)serialization driven by the
declared types, not schema validation of free-form data.
Reach for them when you own the dataclasses and want them filled from a dict and do
not need full validation. Probatio’s DataclassSchema covers the same
dict-to-dataclass path with a richer type mapping and real validation, or its opt-in
construct for trusted input. Among the pure (de)serializers, mashumaro and cattrs
are the fast ones; on the cross-library
benchmark dacite and dataclasses-json are a good deal
slower, so the choice between them is about features and ergonomics, not speed.