Releases: frenck/probatio
v0.3.1: Errata ⚖️
⚖️ Errata
v0.3.0 was discovery: Probatio took in more kinds of evidence. v0.3.1 is the errata sheet. A close audit of the export and import layer turned up places where Probatio's output had drifted from voluptuous, and places where reading an untrusted JSON Schema quietly dropped a constraint. This release corrects the record.
The codecs now match their references. serialize (the voluptuous-serialize field-list shape that config-flow frontends consume) is back to byte-for-byte agreement: In over a mapping, the bare format and transform validators, Maybe, a None-first Any, Enum classes, Coerce of an enum, and a literal value all render the way voluptuous-serialize renders them. to_openapi learned parameterized generics, so list[int] and dict[str, int] become a typed array and object instead of an open schema.
The JSON Schema decoder stopped dropping constraints. from_json_schema now reads contains as "an element matches this subschema" (not "this literal is a member"), keeps a sibling type or range when it sits next to not/anyOf/oneOf/allOf instead of discarding it, honors uniqueItems under an explicit array type, and round-trips an array or object length correctly. Every one of these previously widened an untrusted schema, so this is a fail-closed fix as much as a correctness one.
The drop-in promise got tighter. A schema's extra policy (ALLOW_EXTRA, REMOVE_EXTRA) now reaches dict branches nested inside All, Any, Union, and SomeOf. Schema.extend accepts everything voluptuous accepted, and a marker's description can hold structured data again (the {"suggested_value": ...} dict Home Assistant stores there), not only a string.
⚠️ Behavior change
from_json_schema is now stricter for some external JSON Schemas: a value that slipped through before, because a constraint was being dropped, is correctly rejected now. Round-trips of Probatio's own to_json_schema output are unaffected.
🐍 Python
The support floor drops to Python 3.12, and 3.15 joins the tested set. Probatio now runs on 3.12, 3.13, 3.14, and 3.15.
This is still a 0.x release: a faithful drop-in for voluptuous, validated against its behavior, with some internals still free to move before 1.0.
pip install probatio📚 Docs: https://probatio.frenck.dev
Put your data to the proof. ⚖️
../Frenck
Blogging my personal ramblings at frenck.dev
What's changed
🐛 Bug fixes
- Serialize a literal mapping value as a constant field @frenck (#30)
- Propagate a schema's extra policy into combinator branches @frenck (#31)
- Match voluptuous-serialize output across more validators @frenck (#32)
- Render parameterized generics in the OpenAPI codec @frenck (#33)
- Fix dropped and mis-decoded constraints in the JSON Schema decoder @frenck (#34)
- Widen extend and marker description types for the drop-in promise @frenck (#35)
🧰 Maintenance
v0.3.0: Discovery ⚖️
⚖️ Discovery
v0.1.0 put Probatio on the record, v0.2.0 handed you the tools to cross-examine your test data. v0.3.0 is discovery: Probatio now takes in more kinds of evidence, and is sharper about what it does with them.
The temporal family grew up. AsDatetime, AsDate, and AsTime are the object-returning siblings of Datetime/Date/Time: they parse ISO 8601 out of the box (or a strptime format you pass) and hand you a real datetime/date/time, not the string back. Epoch reads a Unix timestamp into a timezone-aware UTC datetime. And Duration now accepts a plain numeric string as seconds, so "90" and 90 finally mean the same thing.
from probatio import Schema, AsDatetime, Epoch
Schema(AsDatetime())("2026-06-25T10:30:00+02:00")
# datetime.datetime(2026, 6, 25, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)))
Schema(Epoch())(1719571800)
# datetime.datetime(2024, 6, 28, 10, 50, tzinfo=datetime.timezone.utc)New cross-field rules cover the last gap Home Assistant filled by hand: AtLeastOne, AtMostOne, and ExactlyOne say how many of a set of keys may or must appear, the dict-level form of Inclusive/Exclusive.
from probatio import Schema, All, ExactlyOne
Schema(All(dict, ExactlyOne("token", "password")))({"token": "t"}) # unchangedThe typed string validators gained control over their output. MacAddress, E164, HexColor, CreditCard, and IBAN take normalize=False to validate without rewriting, plus upper/separator where they fit. And two compatibility sharpenings: Coerce of an enum lists the valid values again (expected Color or one of 'red', 'green', 'blue'), and the OpenAPI codec emits format: date for Date() instead of date-time.
⚠️ Breaking change
E164, HexColor, CreditCard, and IBAN now normalize by default, so their output changes: grouping is stripped, IBAN is upper-cased, hex color is lower-cased, and E164 also starts accepting formatted numbers like +1 (415) 555-2671. Pass normalize=False to keep the old pass-through behavior. MacAddress is unaffected; it already normalized.
from probatio import Schema, IBAN
Schema(IBAN())("de89 3704 0044 0532 0130 00") # 'DE89370400440532013000'
Schema(IBAN(normalize=False))("de89 3704 0044 0532 0130 00") # unchangedThis is still a 0.x release: a faithful drop-in for voluptuous, validated against its behavior, with some internals still free to move before 1.0.
pip install probatio📚 Docs: https://probatio.frenck.dev
Put your data to the proof. ⚖️
../Frenck
Blogging my personal ramblings at frenck.dev
What's changed
🚨 Breaking changes
✨ New features
- Add AsDatetime/AsDate/AsTime object-returning parsers @frenck (#19)
- Add Epoch validator for Unix timestamps @frenck (#20)
- Add configurable normalization to typed string validators @frenck (#21)
- Add AtLeastOne/AtMostOne/ExactlyOne key-group validators @frenck (#27)
🐛 Bug fixes
- Fix OpenAPI encoder emitting date-time for Date() @frenck (#13)
- List an enum's values in the Coerce error message @frenck (#28)
🚀 Enhancements
🧰 Maintenance
- ⬆️ Update gcr.io/oss-fuzz-base/base-builder-python:latest Docker digest to 66c3a78 @renovate[bot] (#14)
- Upload test results to Codecov for test analytics @frenck (#23)
- Use markdown.processor for the rehype plugin @frenck (#25)
📚 Documentation
- Add a Codecov coverage badge to the README @frenck (#22)
- Split the codec guide into JSON Schema, OpenAPI, and Field lists @frenck (#24)
- Give TypedDict schemas their own guide page @frenck (#26)
⬆️ Dependency updates
12 changes
- ⬆️ Update actions/attest action to v4.1.1 @renovate[bot] (#6)
- ⬆️ Pin CodSpeedHQ/action action to v3.8.1 @renovate[bot] (#5)
- ⬆️ Update dependency ruff to v0.15.20 @renovate[bot] (#8)
- ⬆️ Update release-drafter/release-drafter action to v7.5.1 @renovate[bot] (#12)
- ⬆️ Update actions/attest-build-provenance action to v3.2.0 @renovate[bot] (#11)
- ⬆️ Update dependency syrupy to v5.3.4 @renovate[bot] (#9)
- ⬆️ Update dependency ty to v0.0.55 @renovate[bot] (#10)
- ⬆️ Update actions/setup-node action to v6 @renovate[bot] (#16)
- ⬆️ Update actions/attest-build-provenance action to v4 @renovate[bot] (#15)
- ⬆️ Update gcr.io/oss-fuzz-base/base-builder-python:latest Docker digest to 66c3a78 @renovate[bot] (#14)
- ⬆️ Lock file maintenance @renovate[bot] (#18)
- ⬆️ Update CodSpeedHQ/action action to v4 @renovate[bot] (#17)
v0.2.0: Cross-Examination ⚖️
⚖️ Hands you the tools to put your data on the stand where it matters most: in your tests.
Meet pytest-probatio, a companion package that lets a Probatio schema stand in as a pytest assertion matcher. The schema reads as the expected shape, and a mismatch is explained by Probatio's path-precise errors instead of a bare assert.
from pytest_probatio import Exact, Partial
from probatio import Port
def test_response(response):
# Exact: an extra key makes it unequal.
assert response == Exact({"name": str, "port": Port()})
# Partial: extra keys are allowed.
assert response == Partial({"name": str})When the data does not match, the failure points at the exact offending value, even deep inside a nested or composed shape:
data does not match the probatio schema (==):
data['users'][0]['email']: expected an email address
It is plain pytest output, so a failing schema match flows straight into pytest-github-actions-annotate-failures and becomes a GitHub annotation on the test, with no extra setup. Define a schema once and reuse it across as many tests as you like. It ships as a separate distribution, so the core library stays dependency-free, and it is released lock-step with Probatio at the same version.
pip install pytest-probatioThis release also tightens the evidence. The atheris fuzzers that guard the untrusted-input surfaces now run on every pull request on Python 3.13, not just locally. That is not decoration: turning them loose in CI found and closed a handful of raw-exception leaks (in the OpenAPI and JSON Schema codecs, the IP validators, and Contains). The safe-validator contract, a built-in validator only ever raises Invalid, never a raw exception, is now enforced continuously rather than checked by hand.
📚 Docs: https://probatio.frenck.dev
Put your data to the proof. ⚖️
../Frenck
Blogging my personal ramblings at frenck.dev
What's changed
✨ New features
🐛 Bug fixes
🧰 Maintenance
v0.1.0: Exhibit A ⚖️
⚖️ Exhibit A
Probatio is a modern, maintained, MIT-licensed reimplementation of voluptuous (https://github.com/alecthomas/voluptuous), written clean-room: the same public API, no copied code, so it can be kept alive and moved forward. Change the import, keep your schemas.
This is the first public release, and the point of a validation library is whether you would hand it untrusted input, so here is the evidence rather than the adjectives. voluptuous's own 0.16.0 test suite runs against Probatio at 140 passing and 27 deliberate, documented deviations. Home Assistant's config_validation passes 142 of 142 with voluptuous swapped out. Line and branch coverage sit at 100%, type-checked under both mypy and ty, and every untrusted-input surface is fuzzed: a built-in validator only ever raises Invalid, never a raw exception, and that is enforced, not hoped for. It also runs about 2.2 to 2.6 times faster than voluptuous on typical schemas.
It does not stop at parity. Probatio clears voluptuous's own backlog and keeps going: cross-field rules (RequiredWith, RequiredIf, and friends), dataclass and TypedDict schemas, network and format validators, JSON/YAML/TOML loading and dumping, JSON Schema and OpenAPI in both directions, and errors that carry a path and suggest the key you meant.
from probatio import Schema, Required, Optional
schema = Schema({Required("name"): str, Optional("port", default=8080): int})
schema({"name": "app"})
# {'name': 'app', 'port': 8080}A drop-in swap is usually one import line:
# from voluptuous import Schema, Required, All, Coerce
from probatio import Schema, Required, All, CoerceThis is a 0.x release: the goal is a faithful drop-in, validated against voluptuous's behavior, and some internals may still move before 1.0 as it gathers production feedback.
pip install probatio📚 Docs: https://probatio.frenck.dev
Put your data to the proof. ⚖️
../Frenck
Blogging my personal ramblings at frenck.dev