schemmy
Validate data with plain type annotations. No dependencies.
pip install schemmy
# or
uv add schemmy
Requires Python 3.12+.
Example
from typing import Annotated
from schemmy import Schema, ValidationError
def positive(value: int) -> int:
if value <= 0:
raise ValueError("must be greater than zero")
return value
class Item(Schema):
sku: str
qty: Annotated[int, positive]
class Order(Schema):
id: int
items: list[Item]
order = Order.validate({"id": 1, "items": [{"sku": "A1", "qty": 2}]})
order.items[0].qty # 2
All errors are collected at once, not just the first one. Each error carries the path to the field:
try:
Order.validate({"id": "1", "items": [{"sku": "A1", "qty": 0}, {"qty": 3}], "note": "x"})
except ValidationError as exc:
for error in exc.exceptions:
print(error.location, error.message)
['note'] Unknown field
['id'] Expected int, got str
['items', 0, 'qty'] Validator positive failed: must be greater than zero
['items', 1, 'sku'] Field required
Errors come in a stable order: missing and unknown fields first, then value errors in field declaration order.
ValidationError is an ExceptionGroup of FieldValidationError, so except* FieldValidationError works too. Both errors can be pickled and copied.
What is supported
| Annotation | Behavior |
|---|---|
int, str, float, bool, any class |
isinstance check, no type coercion. bool is rejected for int fields |
float |
Also accepts an int that float represents exactly and converts it, since JSON sends 1 for 1.0 |
Any |
Value is accepted as is |
list[T] |
Each item is checked against T; the item index is added to the error path |
tuple[T, ...], tuple[T1, T2] |
Accepts a tuple or a list and stores a tuple, since JSON has no tuples. A fixed tuple requires the exact length |
dict[K, V] |
Each key is checked against K and each value against V; the key is added to the error path |
Literal[...] |
The value must equal one of the literals and have the same type, so True does not match Literal[1] |
X | Y, Optional[X] |
Members are tried in order, the first match wins. For X | None, errors of X are reported as is. When only one member accepts the value type, its errors are reported |
Schema subclass |
Accepts a schema instance or a dict |
Annotated[T, *validators] |
T is checked first, then validators run in order |
A validator is a callable that takes a value and returns it, possibly changed. To report an error, a validator raises an exception. The first failing validator stops the chain. The returned value is stored in the schema, so validators can normalize data, for example bring a phone number to a single format.
Unknown fields and missing required fields are validation errors as well. Input to Schema.validate that is not a dict produces a single error with an empty location. Input to a recursive schema nested deeper than the Python recursion limit allows produces a single Maximum nesting depth exceeded error.
A field with a default value is optional. If the field is not passed, the schema gets a copy of the default. The default value itself is not validated.
ClassVar annotations are not fields.
Bare typing.List, typing.Dict and typing.Tuple are treated as list[Any], dict[Any, Any] and tuple[Any, ...].
Working with instances
order.to_dict()
# {'id': 1, 'items': [{'sku': 'A1', 'qty': 2}]}
order
# Order(id=1, items=[Item(sku='A1', qty=2)])
Order.validate(order.to_dict()) == order
# True
to_dict converts nested schemas recursively and keeps field declaration order. Schemas compare by class and field values. Schemas are mutable and therefore unhashable.
Type checking
Type checkers derive the constructor signature from the annotations, so wrong types, unknown and missing fields are reported before runtime:
Item(sku=1, qty=2) # error: expected str
Item(sku="A1", qyt=2) # error: unknown argument
The constructor is typed for already typed values. Use Schema.validate for raw data such as parsed JSON, where nested schemas arrive as dicts.
Limitations
set, frozenset, TypedDict and other annotations not listed above are not supported yet. A field with such an annotation produces an "unsupported type" error.
Development
uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check .
uv run ty check
License
MIT
Metadata
Release files for schemmy 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| schemmy-0.2.0.tar.gz | 10.3 kB | Without provenance | Details for schemmy-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| schemmy-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for schemmy-0.2.0-py3-none-any.whl |
Total release size: 20.9 kB
Release files / schemmy-0.2.0.tar.gz
| Download URL | schemmy-0.2.0.tar.gz |
|---|---|
| Size | 10.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d89a5a8f7eadba0dc32e3d1e15fe0e917160bb2a6fb5ba906fa0737dc9ca9744
|
|
BLAKE2b-256 checksum How to use checksums |
4649c8ad12d7faa2425b2ba3912cb0e48539a94a22cdd6ee74e24d366b6d3041
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / schemmy-0.2.0-py3-none-any.whl
| Download URL | schemmy-0.2.0-py3-none-any.whl |
|---|---|
| Size | 10.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
147930cb19ed03218b61e18f7c51562ce8c5bb7e6698cbe86603b2c1256a4bb2
|
|
BLAKE2b-256 checksum How to use checksums |
066574532a35ce94a02e27058d4c6a38df96fd8f2c54848777860a8585dac221
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for schemmy 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.