Skip to main content

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)

Source distribution for schemmy 0.2.0
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)

Table of built distributions (wheels) for schemmy 0.2.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page