Typedpy - Strict Type System for Python
typedpy is a library for type-safe, strict, validated Python data structures, with
serialization, JSON Schema and IDE stub generation built in. A structure can never hold an
invalid value: validation runs when it is created and on every change afterwards,
including changes inside nested lists and dicts.
It is pure Python with zero dependencies, supports Python 3.9-3.15, and has run in production, including financial systems, since 2017.
pip install typedpy
(or conda install -c conda-forge typedpy)
Quick start
import enum
from typing import Optional
from typedpy import (
ImmutableStructure, String, PositiveInt, Float, Enum,
Serializer, Deserializer, mappers,
)
class Currency(enum.Enum):
USD = 1
EUR = 2
class Trader(ImmutableStructure):
lei: String(pattern="[0-9A-Z]{18}[0-9]{2}$")
alias: String(maxLength=32)
_serialization_mapper = mappers.TO_CAMELCASE
class Trade(ImmutableStructure):
order_id: String
symbol: String(pattern="[A-Z]+$", maxLength=6)
quantity: PositiveInt(multiplesOf=5)
price: Float(minimum=0)
currency: Enum[Currency]
buyer: Trader
tags: list[str]
comment: Optional[str]
_serialization_mapper = mappers.TO_CAMELCASE
# deserialize (and validate) JSON-like input; keys are camelCase on the wire
trade = Deserializer(Trade).deserialize({
"orderId": "T-1001",
"symbol": "AAPL",
"quantity": 100,
"price": 231.5,
"currency": "USD",
"buyer": {"lei": "5493001KJTIIGC8Y1R12", "alias": "desk-7"},
"tags": ["equity"],
})
assert trade.currency is Currency.USD
Trade(order_id="T-1", symbol="AAPL", quantity=-5, price=1.0,
currency=Currency.USD, buyer=trade.buyer, tags=[])
# ValueError: Trade.quantity: Got -5; Expected a positive number
trade.quantity = 200 # ValueError: Trade: Structure is immutable
trade.tags.append("bond") # ValueError: tags: Field is immutable (deep immutability)
Serializer(trade).serialize()
# {'orderId': 'T-1001', 'symbol': 'AAPL', 'quantity': 100, ..., 'buyer': {'lei': ..., 'alias': 'desk-7'}}
Fields can be typedpy field types with constraints (String(maxLength=32)), plain Python
types (int, str), standard typing/PEP 585 annotations (list[str], Optional[...]),
other structures, or any mix of them at any depth, e.g. Array[dict[String(minLength=5), int]].
A mutable Structure is still validated on every change:
from typedpy import Structure
class Order(Structure):
quantity: PositiveInt
tags: list[str]
order = Order(quantity=5, tags=["a"])
order.quantity = -1 # ValueError: quantity: Got -1; Expected a positive number
order.tags.append(3) # TypeError: ... Expected a string
Highlights
Deep immutability
ImmutableStructure blocks reassignment and changes to nested lists, dicts and sets,
reads return defensive copies, and later changes to the caller's input don't leak in.
(@dataclass(frozen=True) and Pydantic's frozen only block reassignment.) Individual fields
can be made immutable too (ImmutableField, ImmutableArray, ImmutableMap...).
A field system that is plain object-oriented Python
A custom field is an ordinary class. Constraints are mixins that combine through inheritance:
from typedpy import Field, Integer, Positive
class Even(Field):
def __set__(self, instance, value):
if value % 2:
raise ValueError(f"{self._name}: Got {value}; Expected an even number")
super().__set__(instance, value)
class EvenPositiveInt(Integer, Positive, Even):
pass
class Batch(Structure):
size: EvenPositiveInt
Batch(size=3) # ValueError: Batch.size: Got 3; Expected an even number
Optional hooks plug a custom field into everything else: SerializableField for
serialization, to_json_schema/from_json_schema for JSON Schema, and get_type for stubs.
Classes you don't own can be used directly as field types, or wrapped with
create_typed_field.
Discriminated unions
The discriminator is a real field reference (refactor-safe), and variants are discovered
automatically from subclasses, so there's no Union[...] to forget to update:
from typedpy import AbstractStructure, Constant, DiscriminatedUnion
class Shape(AbstractStructure):
kind: String
class Circle(Shape):
kind = Constant("circle")
radius: float
class Square(Shape):
kind = Constant("square")
side: float
class Drawing(Structure):
shapes: list[DiscriminatedUnion(Shape, by=Shape.kind)]
drawing = Deserializer(Drawing).deserialize(
{"shapes": [{"kind": "circle", "radius": 1.0}, {"kind": "square", "side": 2.0}]}
)
assert [type(s) for s in drawing.shapes] == [Circle, Square]
Unrelated classes work too: DiscriminatedUnion(Union[Circle, Square], by=Circle.kind).
Deriving models from existing ones
Partial, AllFieldsRequired, Omit, Pick and Extend work like TypeScript's utility
types. They copy fields rather than inherit, so they work on immutable classes too:
from typedpy import Partial, Omit
class TradeUpdate(Partial[Omit[Trade, ("buyer", "currency")]]):
pass
TradeUpdate(quantity=50) # every remaining field is optional
Enum-keyed structures: @keys_of
Guarantee at import time that a structure has a field for every member of an enum, like
TypeScript's Record<Role, ...>. Adding an enum member without updating the class fails fast:
from typedpy import keys_of
class Role(enum.Enum):
admin = 1
engineer = 2
sales = 3
@keys_of(Role)
class SalaryBands(Structure):
admin: int
engineer: int
# TypeError: SalaryBands: missing fields: sales
Undefined vs None
Opt in to a real "no value" that is distinct from None, e.g. for PATCH-style APIs:
from typedpy import Undefined
class Patch(Structure):
name: str
email: str
_required = []
_ignore_none = True
_enable_undefined_value = True
patch = Deserializer(Patch).deserialize({"email": None})
assert patch.name is Undefined and patch.email is None
Serializer(patch).serialize() # {'email': None}
Serialization
-
Key mappers (
TO_CAMELCASE,TO_LOWERCASE, rename dicts, function mappers), which can be chained as a list and compose through inheritance. -
Schema versioning: a
Versionedstructure is migrated to its latest schema on deserialization. -
Trusted deserialization for data you already trust (e.g. your own database), which skips validation and is several times faster, with a unit-test helper to make sure a class stays eligible:
trade = Deserializer(Trade).deserialize(row, direct_trusted_mapping=True) # in a unit test: from typedpy.testing import assert_trusted_deserialization_mapper_is_safe assert_trusted_deserialization_mapper_is_safe(Trade)
-
Fast serialization: mark a class
FastSerializableand callcreate_serializer(cls)for roughly 4-5x faster serialization, still in pure Python. -
Pickling support.
JSON Schema, in both directions
from typedpy import structure_to_schema, schema_to_struct_code
schema, definitions = structure_to_schema(Trader, {})
print(schema_to_struct_code("Trader", schema, definitions))
# class Trader(Structure):
# lei: String(pattern='[0-9A-Z]{18}[0-9]{2}$')
# alias: String(maxLength=32)
#
# _required = ['alias', 'lei']
IDE and type-checker support through generated stubs
typedpy generates .pyi stubs that turn its field types back into ordinary type hints, with
full __init__ signatures, so any IDE or type checker understands your structures (no plugin
needed):
create-stubs-for-dir <src_root_dir> <directory>
create-stub <src_root_dir> <path/to/module.py>
Also included
Structured, collectable errors (ErrorInfo), cross-field validation via __validate__,
AbstractStructure/FinalStructure, shallow_clone_with_overrides, deep_get, typedpy.testing.find_diff,
@default_factories, and global defaults through TypedPyDefaults.
Why pure Python?
typedpy is deliberately pure Python with no dependencies: you get full stack traces, it can be debugged and audited end to end, there are no compiled builds, and the supply-chain surface is just this package. That suits regulated and correctness-critical settings.
The trade-off is speed: validation is slower than Pydantic's Rust core. If validation dominates your workload, or you need Pydantic's ecosystem (FastAPI, SQLModel, settings), Pydantic is the better fit. typedpy's strengths are elsewhere: deep immutability, validation on every change, a simple extension model, and the features above. It also offers trusted deserialization and fast serialization for the hot paths.
Documentation
Full documentation: typedpy.readthedocs.io, including a tutorial, structures, fields, serialization, JSON Schema, stubs and limitations.
The tests are also a large collection of working examples.
License
MIT. See LICENSE.txt.
Metadata
Release files for typedpy 2.31.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 |
|---|---|---|---|---|
| typedpy-2.31.0.tar.gz | 159.9 kB | Without provenance | Details for typedpy-2.31.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| typedpy-2.31.0-py3-none-any.whl | Python 3 | none | any | Details for typedpy-2.31.0-py3-none-any.whl |
Total release size: 266.9 kB
Release files / typedpy-2.31.0.tar.gz
| Download URL | typedpy-2.31.0.tar.gz |
|---|---|
| Size | 159.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c51b79a9c6ead1666ea78be5119dc49212ee69df552c58e6ec9bac73f9028909
|
|
BLAKE2b-256 checksum How to use checksums |
9d770372f373fc774356e31c67adebeb401e92626a4ad82101d41998a66c8e4b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / typedpy-2.31.0-py3-none-any.whl
| Download URL | typedpy-2.31.0-py3-none-any.whl |
|---|---|
| Size | 107.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
061067585c7c689ddf4c85c8ea793915bba6c3b01524a7bdd70247bec236dd0a
|
|
BLAKE2b-256 checksum How to use checksums |
f01dfec588bb4dc5a0548b9e1850eb012bbdb47029a0e9c917c6fe6a02e6a99b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Security for typedpy 2.31.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.