Skip to content

Releases: frenck/probatio

v0.3.1: Errata ⚖️

28 Jun 14:58
Immutable release. Only release title and notes can be modified.
f905db9

Choose a tag to compare

⚖️ 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 ⚖️

28 Jun 11:41
Immutable release. Only release title and notes can be modified.
0a4c0bc

Choose a tag to compare

⚖️ 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"})  # unchanged

The 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")  # unchanged

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

🚨 Breaking changes

  • Add configurable normalization to typed string validators @frenck (#21)

✨ 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

  • Accept a numeric string as seconds in Duration @frenck (#29)

🧰 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

v0.2.0: Cross-Examination ⚖️

27 Jun 23:45
Immutable release. Only release title and notes can be modified.
af8c568

Choose a tag to compare

⚖️ 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-probatio

This 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

  • Add pytest-probatio companion package (uv workspace) @frenck (#3)

🐛 Bug fixes

  • Run atheris fuzzers in CI on Python 3.13, fix the leaks it found @frenck (#4)

🧰 Maintenance

  • Run atheris fuzzers in CI on Python 3.13, fix the leaks it found @frenck (#4)

v0.1.0: Exhibit A ⚖️

27 Jun 20:24
Immutable release. Only release title and notes can be modified.
ef29864

Choose a tag to compare

⚖️ 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, Coerce

This 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