Built-in validators
A validator is a callable that takes a value and returns it (possibly
normalized), or raises Invalid. Probatio ships a stack of them so you rarely
have to write your own. This page walks the toolbox by category, with a short
runnable example for nearly all of them.
These are the leaf validators. The combinators that compose them (All, Any,
Union, SomeOf) live in the combinators guide, and the
dictionary markers (Required, Optional, and friends) in the dict schemas and
markers guide. You will see those names in
passing here; this page is about the leaves. For the whole toolbox at a glance,
including the transformers that change a value rather than check it, see
Built-ins by role.
Two naming conventions run through the toolbox, so a name tells you what it does
with the value. A validator named As<Type> (AsDatetime, AsTimedelta)
validates the input and returns a real object of that type; its plain sibling
(Datetime, Duration) checks the same input but returns it unchanged,
string-in string-out, for voluptuous compatibility. A validator named
From<Source> (FromEpoch, FromPercentage, FromJSONString) builds a value
from that source form. So As names the type you get out, From names the shape
that goes in.
Type and value
Section titled “Type and value”These check what a value is, or what it equals.
Coerce converts with type(value) and reports a clean CoerceInvalid when the
conversion fails. Boolean() reads common truthy and falsy strings ("yes",
"on", "off", and such) as a bool; it is a factory, so call it. Literal
and Equal pin a value to a constant. In and NotIn test membership,
Contains tests the other direction, and Match runs a regular expression.
from probatio import Schema, Coerce, Boolean, Literal, Equal
Schema(Coerce(int))("42") # 42Schema(Boolean())("on") # TrueSchema(Boolean())("off") # FalseSchema(Literal("on"))("on") # 'on'Schema(Equal(3))(3) # 3from probatio import Schema, In, NotIn, Contains, Match
Schema(In(["red", "green", "blue"]))("green") # 'green'Schema(NotIn(["root", "admin"]))("frenck") # 'frenck'Schema(Contains(2))([1, 2, 3]) # [1, 2, 3]Schema(Match(r"^[a-z]+$"))("probatio") # 'probatio'Coerce raises when the value cannot be converted:
from probatio import Schema, Coerce
Schema(Coerce(int))("not a number")Numbers
Section titled “Numbers”Numeric bounds and conversions.
Range checks that a value falls within min and max. Both endpoints are
inclusive by default; set min_included=False or max_included=False for an
open bound. Clamp pins a value into the range instead of failing. Number
validates a numeric string, optionally checking its precision (significant
digits) and scale (decimal places).
from probatio import Schema, Range, Clamp, Number
Schema(Range(min=1, max=10))(5) # 5Schema(Range(min=0, max=1, max_included=False))(0.5) # 0.5Schema(Clamp(min=0, max=100))(150) # 100Schema(Number(precision=4, scale=2))("12.34") # '12.34'Positive, Negative, and NonNegative are sign conveniences over Range.
Byte (0 to 255) and SmallFloat (0 to 1) are bounded ranges, and Latitude
(-90 to 90) and Longitude (-180 to 180) bound geographic coordinates.
MultipleOf requires an integer multiple. Percentage validates a number, a bare
numeric string, or a "NN%" string in 0 to 100 and returns it unchanged;
FromPercentage parses it to a float. None of these coerce the type otherwise; wrap
with Coerce for that (All(Coerce(int), NonNegative())).
from probatio import ( Schema, Positive, MultipleOf, Percentage, FromPercentage, Latitude, Longitude,)
Schema(Positive())(5) # 5Schema(MultipleOf(15))(45) # 45Schema(Percentage())("80%") # '80%'Schema(FromPercentage())("80%") # 80.0Schema(Latitude())(52.37) # 52.37Schema(Longitude())(4.9) # 4.9A small family of arithmetic mutators rescales a number, the readable stand-in for
the Coerce(lambda v: v / 1000) calculations that pile up on device and sensor
fields. Multiply and Divide apply a gain or a unit conversion, Offset shifts a
value, and Round tidies the result (to ndigits decimals, or to the nearest
integer by default). An integer operand on an int keeps an int; a Divide or a
fractional operand yields a float. Chain them in an All when a calculation takes
more than one step.
from probatio import Schema, All, Multiply, Divide, Offset, Round
Schema(Divide(1000))(5000) # 5.0 (milliunits to units)Schema(Multiply(0.1))(50) # 5.0 (a gain)Schema(Offset(50))(100) # 150 (a shift, stays an int)Schema(Round(2))(5.126) # 5.13Schema(All(Offset(-273.15), Round(2)))(300) # 26.85 (Kelvin to Celsius, chained)Remap linearly maps a number from one range onto another, the Arduino map(). It
is the workhorse for a raw reading: a 0 to 1023 ADC or a 0 to 255 byte to a
percentage, or an RSSI in dBm to a signal percentage. It does not clamp, so wrap it
in Clamp to bound the result, and add Round to tidy it.
from probatio import Schema, All, Remap, Clamp, Round
Schema(All(Remap(0, 255, 0, 100), Round(1)))(128) # 50.2 (a byte as a percentage)Schema(All(Remap(-100, -50, 0, 100), Clamp(0, 100)))(-70) # 60.0 (RSSI dBm to percent)Scale folds the whole affine transform into one call, value * factor / divisor + offset, optionally rounded, when a single field does several steps at once.
from probatio import Schema, Scale
Schema(Scale(divisor=1000))(5000) # 5.0 (milliunits to units)Schema(Scale(10))(5) # 50 (integer gain, stays an int)Schema(Scale(100, divisor=255, round=1))(128) # 50.2 (a byte as a percentage)Schema(Scale(offset=-273.15, round=2))(300) # 26.85 (Kelvin to Celsius)A few finishers round out the number work. Round takes the nearest value; RoundUp
and RoundDown take the ceiling and floor (to a whole integer). Snap rounds to the
nearest multiple of a step, for a setpoint. Abs is the magnitude, and Modulo wraps
a value, so Modulo(360) keeps a heading in 0 to 359 (MultipleOf, by contrast,
validates divisibility rather than transforming).
from probatio import Schema, RoundUp, RoundDown, Snap, Abs, Modulo
Schema(RoundUp())(4.1) # 5Schema(RoundDown())(4.9) # 4Schema(Snap(0.5))(1.2) # 1.0 (nearest half)Schema(Abs())(-3) # 3Schema(Modulo(360))(370) # 10 (wrap a heading)Collections and structure
Section titled “Collections and structure”These shape sequences, sets, and objects.
Length bounds the length of a sized value. Unique requires distinct items.
Set turns an iterable into a set (handy because JSON has no set type, so sets
arrive as lists). ExactSequence validates a fixed-length sequence position by
position, Unordered does the same but lets the items appear in any order.
Object validates an object’s attributes like a mapping. Maybe allows None,
otherwise it defers to the wrapped validator. EnsureList normalizes the common
“one value or a list of them” shape: a scalar becomes a single-item list, a list
passes through, and None becomes an empty list. NonEmpty requires a sized
value (string, list, mapping) to not be empty.
from probatio import Schema, Length, Unique, ExactSequence, Unordered, Maybe, EnsureList
Schema(Length(min=1, max=3))([1, 2]) # [1, 2]Schema(Unique())([1, 2, 3]) # [1, 2, 3]Schema(ExactSequence([str, int]))(["a", 1]) # ['a', 1]Schema(Unordered([str, int]))([1, "a"]) # [1, 'a']Schema(Maybe(int))(None) # NoneSchema(Maybe(int))(5) # 5Schema(EnsureList())("one") # ['one']Maybe composes with a coercer for the “optional, coerce if present” field:
Maybe(Coerce(float)) leaves None alone and coerces anything else, so there is
no null check to hand-write:
from probatio import Schema, Maybe, Coerce
Schema(Maybe(Coerce(float)))(None) # NoneSchema(Maybe(Coerce(float)))("0.5") # 0.5Sorted requires a collection to already be in order (it does not reorder):
from probatio import Schema, Sorted
Schema(Sorted())([1, 2, 3]) # [1, 2, 3]A handful of shapers rewrite a list instead of validating it. Split cuts a
delimited string into a list (stripping pieces and dropping empties by default), and
Join is its inverse. Sort orders a list and Dedupe drops repeats, keeping
first-seen order, the transformer siblings of Sorted and Unique. First and
Last pick an element, and Without prunes listed values, so Without(None) clears
the holes.
from probatio import Schema, Split, Join, Sort, Dedupe, First, Last, Without
Schema(Split(","))("a, b ,c") # ['a', 'b', 'c']Schema(Join(","))([1, 2, 3]) # '1,2,3'Schema(Sort())([3, 1, 2]) # [1, 2, 3]Schema(Dedupe())([1, 2, 1, 3]) # [1, 2, 3]Schema(First())([1, 2, 3]) # 1Schema(Last())([1, 2, 3]) # 3Schema(Without(None, 0))([1, None, 0, 2]) # [1, 2]Set returns a real set, so its repr ordering is not stable:
from probatio import Schema, Set
result = Schema(Set())([1, 2, 2, 3])sorted(result) # [1, 2, 3]Object rebuilds an object of the same type after validating its attributes:
from probatio import Schema, Object
class Point: def __init__(self, x, y): self.x, self.y = x, y
result = Schema(Object({"x": int, "y": int}))(Point(1, 2))result.x # 1result.y # 2Strings
Section titled “Strings”Two kinds live here: transforms that rewrite the value, and format validators that check it.
The transforms (Lower, Upper, Capitalize, Title, Strip) are plain
functions, not classes. Use them bare, Lower, not Lower(). Replace
substitutes a pattern. The format validators (Email(), Url(), FqdnUrl())
are factories, so you call them to build the validator, matching voluptuous.
They avoid backtracking regular expressions, so a crafted input cannot hang them.
from probatio import Schema, Lower, Upper, Capitalize, Title, Strip, Replace
Schema(Lower)("HELLO") # 'hello'Schema(Upper)("hello") # 'HELLO'Schema(Capitalize)("hello world") # 'Hello world'Schema(Title)("hello world") # 'Hello World'Schema(Strip)(" hi ") # 'hi'Schema(Replace("-", "_"))("a-b-c") # 'a_b_c'A few more transforms take an argument, so they are classes you call.
CollapseWhitespace squeezes each run of whitespace to one space (Strip only
trims the ends). RemovePrefix and RemoveSuffix strip a known affix when present,
and Truncate caps the length. Unlike the plain transforms above, these reject a
non-string rather than coercing it.
from probatio import Schema, CollapseWhitespace, RemovePrefix, RemoveSuffix, Truncate
Schema(CollapseWhitespace())(" a b c ") # 'a b c'Schema(RemovePrefix("sensor."))("sensor.temp") # 'temp'Schema(RemoveSuffix("_raw"))("value_raw") # 'value'Schema(Truncate(5))("hello world") # 'hello'from probatio import Schema, Email, Url, FqdnUrl
Schema(Url())("https://example.com/path") # 'https://example.com/path'Schema(FqdnUrl())("https://example.com") # 'https://example.com'Slug validates a slug (lowercase alphanumerics with hyphen or underscore
separators), returning it unchanged. It checks the shape; it does not slugify
arbitrary text (transliteration belongs to a dedicated package, reached with
Coerce). IsRegex checks that the value is itself a compilable regular
expression (it validates the pattern, it does not run it).
from probatio import Schema, Slug
Schema(Slug())("my-config_key-2") # 'my-config_key-2'More string checks: the character classes Alpha, Alphanumeric, ASCII,
PrintableASCII, and NoWhitespace; the affix checks StartsWith and
EndsWith; ByteLength (UTF-8 bytes, not code points); and HexColor.
from probatio import Schema, Alphanumeric, StartsWith, HexColor
Schema(Alphanumeric())("abc123") # 'abc123'Schema(StartsWith("https://"))("https://example.com") # 'https://example.com'Schema(HexColor())( "#FF8800") # '#FF8800' (validated, unchanged; use Lower to fold case)Format and checksum
Section titled “Format and checksum”A handful of validators check a structured format or a checksum, all pure (no
network, no extra dependency). Like the network validators below, these are
Probatio additions with no voluptuous equivalent: CreditCard (the Luhn
check), IBAN (the ISO 13616
mod-97 check), DataURI (an RFC 2397 data: URI), and E164 (a phone number in
international format). By default CreditCard, IBAN, and E164 canonicalize the
input (strip grouping, upper-case the IBAN); pass normalize=False to validate and
return the value unchanged.
from probatio import Schema, CreditCard, IBAN, E164
Schema(CreditCard())("4242 4242 4242 4242") # '4242424242424242'Schema(IBAN())("de89 3704 0044 0532 0130 00") # 'DE89370400440532013000'Schema(E164())("+1 (415) 555-2671") # '+14155552671'Schema(E164(normalize=False))("+14155552671") # '+14155552671' (unchanged)These check shape, not existence: CreditCard confirms the Luhn checksum, not that
the card is real, and E164 confirms the international format, not that the number
is dialable (which needs a phone-number database).
Date and time
Section titled “Date and time”Datetime and Date validate a string against a strptime format, returning
the string unchanged when it parses. Datetime defaults to ISO 8601
(%Y-%m-%dT%H:%M:%S.%fZ), Date defaults to %Y-%m-%d. Pass format= for
anything else.
from probatio import Schema, Datetime, Date
Schema(Datetime())("2026-06-25T10:30:00.000000Z") # '2026-06-25T10:30:00.000000Z'Schema(Date())("2026-06-25") # '2026-06-25'Schema(Date(format="%d/%m/%Y"))("25/06/2026") # '25/06/2026'Time is the time-of-day sibling, defaulting to %H:%M:%S. Duration validates a
duration (a timedelta, a number of seconds as an int/float/numeric string, a
H:MM[:SS] string, an ISO 8601 duration like P1DT2H30M, or a timedelta keyword
mapping) and returns the value unchanged; AsTimedelta accepts the same forms and
returns the parsed datetime.timedelta, the same Datetime/AsDatetime split. An
ISO 8601 duration is limited to the fields a timedelta can represent (weeks, days,
hours, minutes, seconds); a year or a month is rejected, since neither is a fixed
length.
For time zones, TimeZoneInfo validates an IANA name (for the ZoneInfo
object itself, wrap the type: Coerce(zoneinfo.ZoneInfo); its constructor
takes the name). TimeZone validates a fixed UTC offset (+01:00, Z,
UTC), with AsTimezone as the object-returning sibling that parses the
offset into a datetime.timezone.
import zoneinfo
from probatio import ( Schema, Time, Duration, AsTimedelta, TimeZone, TimeZoneInfo, AsTimezone, Coerce,)
Schema(Time())("14:30:00") # '14:30:00'Schema(Duration())("1:30:00") # '1:30:00'Schema(AsTimedelta())("1:30:00") # datetime.timedelta(seconds=5400)Schema(AsTimedelta())("P1DT2H30M") # datetime.timedelta(days=1, seconds=9000)Schema(TimeZoneInfo())("Europe/Amsterdam") # 'Europe/Amsterdam'Schema(Coerce(zoneinfo.ZoneInfo))( "Europe/Amsterdam") # zoneinfo.ZoneInfo(key='Europe/Amsterdam')Schema(TimeZone())("+01:00") # '+01:00'Schema(AsTimezone())("+01:00") # datetime.timezone(datetime.timedelta(seconds=3600))AsDatetime, AsDate, and AsTime are the object-returning siblings of
Datetime, Date, and Time. They return the parsed datetime, date, or
time instead of the original string, and they parse ISO 8601 out of the box, so
no format= is needed for the common case. Pass format= to parse a specific
strptime layout instead. AsDatetime takes require_timezone=True to reject a
naive result; the ISO default reads the offset, so that needs no extra format. A
native object passes straight through, so a datetime, date, or time that a
YAML or TOML loader already produced validates without a round-trip (and AsDate
rejects a datetime, since it carries a time a pure date would drop).
from probatio import Schema, AsDatetime, AsDate, AsTime
Schema(AsDate())("2026-06-25") # datetime.date(2026, 6, 25)Schema(AsTime())("14:30:00") # datetime.time(14, 30)Schema(AsDatetime())("2026-06-25T10:30:00+02:00")# datetime.datetime(2026, 6, 25, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)))Schema(AsDatetime(format="%d/%m/%Y %H:%M"))("25/06/2026 10:30")# datetime.datetime(2026, 6, 25, 10, 30)Reach for these when the next step wants a real object instead of a string;
Datetime/Date/Time stay string-in, string-out for voluptuous compatibility.
Parsing uses the standard library on purpose: a faster backend like ciso8601
accepts a different set of strings and returns a different tzinfo type, which
would make validation depend on what happens to be installed.
FromEpoch reads a Unix timestamp (an int or float) into a timezone-aware UTC
datetime. It takes unit="seconds" by default or unit="milliseconds". The
result is always UTC: a naive, local datetime would depend on the host’s time
zone, so the same input would validate to a different moment on a different
machine.
from probatio import Schema, FromEpoch
Schema(FromEpoch())(1719571800)# datetime.datetime(2024, 6, 28, 10, 50, tzinfo=datetime.timezone.utc)Schema(FromEpoch(unit="milliseconds"))(1719571800000)# datetime.datetime(2024, 6, 28, 10, 50, tzinfo=datetime.timezone.utc)Network and identifiers
Section titled “Network and identifiers”These have no voluptuous equivalent; they are Probatio additions. They validate
and return the value unchanged: IPv4Address/IPv6Address/IPAddress/IPNetwork
check the address or network, UUID checks a UUID, MacAddress a MAC address,
Hostname and Fqdn a name, and Port returns an int. When you want the parsed
Python object rather than the validated string, wrap the type with Coerce (its
constructor takes the string), and use NormalizeMacAddress for a canonical MAC.
from probatio import Schema, IPAddress, UUID, MacAddress, Port, ULID
Schema(IPAddress())("192.0.2.1") # '192.0.2.1'Schema(UUID())("12345678-1234-5678-1234-567812345678")# '12345678-1234-5678-1234-567812345678'Schema(MacAddress())("AA-BB-CC-DD-EE-FF") # 'AA-BB-CC-DD-EE-FF'Schema(Port())("8080") # 8080Schema(ULID())("01ARZ3NDEKTSV4RRFFQ69G5FAV") # '01ARZ3NDEKTSV4RRFFQ69G5FAV'Reach for Coerce when you want the object instead of the validated string:
import ipaddressfrom uuid import UUID as UUIDType
from probatio import Schema, Coerce
Schema(Coerce(ipaddress.IPv4Address))("192.0.2.1") # IPv4Address('192.0.2.1')Schema(Coerce(UUIDType))("12345678-1234-5678-1234-567812345678")# UUID('12345678-1234-5678-1234-567812345678')NormalizeMacAddress rewrites a MAC to a canonical form, lowercase and
colon-separated by default. Pass upper=True or a different separator ("" for
bare hex); MacAddress itself only validates and returns the input unchanged:
from probatio import Schema, NormalizeMacAddress
Schema(NormalizeMacAddress())("AA-BB-CC-DD-EE-FF") # 'aa:bb:cc:dd:ee:ff'Schema(NormalizeMacAddress(upper=True))("aa-bb-cc-dd-ee-ff") # 'AA:BB:CC:DD:EE:FF'Schema(NormalizeMacAddress(separator="-"))("aabbccddeeff") # 'aa-bb-cc-dd-ee-ff'IPNetwork accepts host bits (it validates, it does not normalize to the network);
Hostname takes a bare label, Fqdn requires a dotted name:
from probatio import Schema, IPNetwork, Hostname, Fqdn
Schema(IPNetwork())("192.0.2.5/24") # '192.0.2.5/24'Schema(Hostname())("localhost") # 'localhost'Schema(Fqdn())("host.example.com") # 'host.example.com'Encoding
Section titled “Encoding”JSONString validates that a string is valid JSON (and, with an inner schema, that
the decoded value matches it) and returns the string unchanged. FromJSONString is
the decoding sibling: it parses the string with the standard library’s json and
returns the decoded value.
from probatio import Schema, JSONString, FromJSONString
Schema(JSONString())('{"a": 1, "b": [2, 3]}') # '{"a": 1, "b": [2, 3]}'Schema(FromJSONString())('{"a": 1, "b": [2, 3]}') # {'a': 1, 'b': [2, 3]}Schema(FromJSONString({"port": int}))('{"port": 8080}') # {'port': 8080}Base64 and Hex validate that a string is a valid encoding (they return it
unchanged; decode it yourself with Coerce if you want the bytes).
from probatio import Schema, Base64, Hex
Schema(Base64())("aGVsbG8=") # 'aGVsbG8='Schema(Hex())("deadbeef") # 'deadbeef'HexInt is the parsing sibling of Hex: it reads a hexadecimal string (a
leading 0x is optional) and returns the int.
from probatio import Schema, HexInt
Schema(HexInt())("ff") # 255Schema(HexInt())("0x1A") # 26Filesystem
Section titled “Filesystem”IsDir, IsFile, and PathExists check a path on disk, with IsSymlink,
IsSocket, IsFifo, and IsBlockDevice for the special file types. Because they
touch the real filesystem, the example below creates a temporary directory and
file first.
import osimport tempfilefrom probatio import Schema, IsDir, IsFile, PathExists
with tempfile.TemporaryDirectory() as path: file_path = os.path.join(path, "config.yaml") open(file_path, "w").close()
Schema(IsDir())(path) # an existing directory Schema(IsFile())(file_path) # an existing file Schema(PathExists())(path) # any existing pathTruthiness
Section titled “Truthiness”IsTrue requires a truthy value, IsFalse a falsy one. Both return the value
unchanged.
from probatio import Schema, IsTrue, IsFalse
Schema(IsTrue())(1) # 1Schema(IsFalse())(0) # 0Cross-field rules
Section titled “Cross-field rules”A marker annotates one key. These three look at the whole mapping, so you place
them after a dict schema with All: the dict validates first, then the rule sees
the validated mapping.
RequiredWith makes keys conditional on a trigger key being present:
RequiredWith("tls", "cert", "key") requires cert and key whenever tls is
in the mapping. RequiredWithout is the mirror, requiring keys when a trigger is
absent: RequiredWithout("cert", "cert_path") requires cert_path when cert
is not given. RequiredIf keys on a value: RequiredIf({"auth": "token"}, "token")
requires the token key only when auth equals "token". When the rule does not
fire, nothing is required and the mapping passes through unchanged.
Each takes several triggers (or conditions) with a mode. For the presence rules,
pass a list of trigger keys with mode="any" (the default, any one fires) or
mode="all" (every trigger must hold). For RequiredIf, several conditions
combine with mode="all" (the default, every condition must match) or
mode="any".
from probatio import Schema, All, Optional, RequiredWith
schema = Schema( All( {"tls": bool, Optional("cert"): str, Optional("key"): str}, RequiredWith("tls", "cert", "key"), ),)schema({"tls": True, "cert": "c", "key": "k"}) # unchangedschema({}) # unchanged, no triggerCheck runs an arbitrary predicate over the value with a paired message. A falsy
result, or a predicate that raises (a missing key, a wrong type), is reported with
that message, so a cross-field rule never leaks a raw exception.
from probatio import Schema, All, Check
schema = Schema( All( {"start": int, "end": int}, Check(lambda d: d["start"] < d["end"], "start must be before end"), ),)schema({"start": 1, "end": 2}) # unchangedFor a plain predicate on a single value, rather than a whole mapping, the
truth decorator (a voluptuous-compat helper) turns it into a validator; the
custom validators guide covers it.
AtLeastOne, AtMostOne, ExactlyOne, and AllOrNone are the key-group
presence rules: how many of a set of keys may or must appear. AtLeastOne("host", "url") requires one of the keys (or both); AtMostOne("include", "exclude")
allows one at most; ExactlyOne("token", "password") requires one and only one;
AllOrNone("lat", "lon") requires either all of the keys or none of them. They are
the dict-level form of Inclusive/Exclusive, handy when the constraint reads
more naturally as a standalone check than as a marker on each key.
from probatio import Schema, AtLeastOne, AtMostOne, ExactlyOne, AllOrNone
Schema(AtLeastOne("host", "url"))({"host": "nas"}) # unchangedSchema(AtMostOne("include", "exclude"))({"include": 1}) # unchangedSchema(ExactlyOne("token", "password"))({"token": "t"}) # unchangedSchema(AllOrNone("lat", "lon"))({"lat": 1, "lon": 2}) # unchangedA non-mapping is rejected by default, so the rule stands on its own without an
enclosing dict schema. Pass require_mapping=False to leave a non-mapping
untouched when the rule sits inside a larger pipeline. When more than one key
conflicts (or, for AllOrNone, when a key is missing its partners), each offending
key is reported with its own path.
Transition validators
Section titled “Transition validators”When you validate an update, some fields must not change. Immutable and
WriteOnce compare the new data against its previous value, which you pass as the
call’s context (see call-time context). The same
compiled schema then checks every update. Immutable rejects any change to a
field; WriteOnce lets a field be set once (from absent or None) and freezes it
after. With no context (a first validation) they do nothing.
from probatio import Schema, All, Required, Optional, Immutable, MultipleInvalid
schema = Schema(All({Required("id"): int, Optional("name"): str}, Immutable("id")))
old = {"id": 1, "name": "ada"}schema({"id": 1, "name": "bob"}, context=old) # name may change, unchanged otherwise
try: schema({"id": 2}, context=old)except MultipleInvalid as err: print(err.errors[0].path) # ['id'], with the message "'id' cannot be changed"Defaults and messages
Section titled “Defaults and messages”These do not reject values; they patch them.
DefaultTo replaces None with a default and passes everything else through.
EmptyToNone is the near-reverse: an empty string or container ("", [], {})
becomes None, the “empty means unset” shape from config and forms, while a falsy
scalar like 0 stays put. SetTo ignores the input and always produces a fixed
value. Msg wraps another validator and swaps its failure message for one you
choose, which is the simplest way to make an error read in your own words.
from probatio import Schema, DefaultTo, EmptyToNone, SetTo
Schema(DefaultTo("fallback"))(None) # 'fallback'Schema(DefaultTo("fallback"))("value") # 'value'Schema(EmptyToNone())("") # NoneSchema(EmptyToNone())(0) # 0Schema(SetTo(42))("anything") # 42Map translates a value through a table you supply, like a device status code to a
name. It is the bring-your-own-mapping answer to a domain conversion: probatio does
not pick the table, so a disputed or app-specific lookup stays yours. A value not in
the table is rejected, unless you pass a default.
from probatio import Schema, Map
Schema(Map({0: "off", 1: "on", 2: "auto"}))(1) # 'on'Schema(Map({0: "off"}, default="unknown"))(9) # 'unknown'A fixed default folds every miss to one value. To rewrite only the keys you list
and leave everything else untouched, pass default=PASSTHROUGH. That is the
difference between “unknown means this value” and “only touch what I named”, which is
what you want when a table just cleans up a few sentinel strings ahead of a stricter
validator.
from probatio import Schema, Map, PASSTHROUGH
sentinels = Map({"N.v.t.": None, "n/a": None}, default=PASSTHROUGH)Schema(sentinels)("N.v.t.") # NoneSchema(sentinels)("Personenauto") # 'Personenauto' (unmapped, left as-is)Msg rewrites the message on failure:
from probatio import Schema, Msg, Range
Schema(Msg(Range(min=10), "too small"))(5) # raises with: too smallWhere to next
Section titled “Where to next”- Combinators: compose these with
All,Any,Union, andSomeOf. - Dict schemas and markers:
Required,Optional, defaults, and extra-key policy. - API reference: every name and signature in one place.