Comparison¶
The useful question is which semantics and ecosystem fit a boundary, not which library wins a universal ranking.
| Option | Best fit | Validation/conversion | Runtime footprint | Schemas/JSON | Maturity |
|---|---|---|---|---|---|
| dataclasses alone | typed Python storage | none unless written separately | standard library | manual | standard and mature |
| dataclass + Talea Contract | stdlib domain record with explicit boundaries | exact current state; Mapping/JSON construction | pure Python, zero required runtime dependencies | built in | Talea 0.x; evolving API |
| TypedDict | static dictionary shapes | static only | standard library | manual | standard and mature |
| Talea | strict contracts with explicit external boundaries | strict Python; schema-aware Mapping/JSON conversion | pure Python, zero required runtime dependencies | built in | 0.x, smaller ecosystem |
| Pydantic | broad validation/coercion ecosystem and integrations | configurable; coercive by default with strict modes | Python plus pydantic-core |
built in | large mature ecosystem |
| msgspec | high-throughput serialization and structured decoding | library-specific strict/conversion semantics | native extension | built in | mature focused ecosystem |
| manually written Python | narrow bespoke hot path or unusual semantics | exactly what is implemented | no framework dependency | manual | maintenance owned by application |
Where alternatives are stronger¶
Dataclasses are simpler when runtime validation is unnecessary. TypedDict works well when only static dictionary shape matters. Pydantic has a much larger ecosystem, older-version support, settings and integration packages, callable validation, and established migration knowledge. msgspec has a mature native codec implementation and excellent serialization throughput. Manually written Python can be smaller and faster for one fixed operation.
Talea's distinct choice¶
Talea combines pure-Python, zero-required-runtime-dependency deployment with compile-once execution, strict ordinary construction, explicit external conversion, immutable records, arbitrary Contract roots, rich errors, standards projection, and finite input policies. Those choices are useful when dependency surface and predictable boundaries matter more than ecosystem breadth or implicit coercion.
Benchmark numbers are not reproduced here. See Performance for methodology and semantic caveats, and Adoption for concept mappings.
The same small strict record¶
Code shape can clarify vocabulary, but it is not a benchmark and line count is not a capability score. These representative declarations aim for an immutable record with strict integer/name validation and unknown-field rejection where the library supports it.
Manually written Python¶
class User:
__slots__ = ("id", "name")
def __init__(self, *, id: int, name: str) -> None:
if type(id) is not int:
raise TypeError("id must be int")
if type(name) is not str:
raise TypeError("name must be str")
object.__setattr__(self, "id", id)
object.__setattr__(self, "name", name)
def __setattr__(self, name: str, value: object) -> None:
raise AttributeError("User is immutable")
This is explicit and attractive for one tiny internal path. Mapping conversion, JSON representations, nested locations, serialization, and schemas would need additional application code if required.
Dataclass plus manual validation¶
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class User:
id: int
name: str
def __post_init__(self) -> None:
if type(self.id) is not int or type(self.name) is not str:
raise TypeError("invalid User")
The dataclass owns record mechanics. When those boundaries are needed without
rewriting the domain type, Contract(User) can own conversion, structured
errors, JSON, and schemas while preserving this class and its lifecycle.
Pydantic¶
from pydantic import BaseModel, ConfigDict
class User(BaseModel):
model_config = ConfigDict(strict=True, frozen=True, extra="forbid")
id: int
name: str
This deliberately enables strict and frozen behavior rather than presenting a poor default-only comparison. Pydantic additionally brings its mature model, schema, settings, integration, and plugin ecosystems.
msgspec¶
import msgspec
class User(msgspec.Struct, frozen=True, forbid_unknown_fields=True):
id: int
name: str
msgspec's native codecs and Struct representation are central to its design. Its exact conversion, supported types, validation, and error semantics should be evaluated against the real workload rather than inferred from syntax.
Talea¶
Talea's class is strict, frozen, keyword-only, slotted, Mapping/JSON capable, serializable, and schema-projectable by contract. That larger built-in surface is useful only if the application needs it.
Scenario-based selection¶
- A FastAPI estate with Pydantic-specific plugins, settings models, and staff expertise should usually keep the mature integration path unless a measured boundary need justifies isolation.
- A high-throughput service whose supported payloads and native codec workflow align exactly with msgspec should use that strength rather than changing for novelty.
- An internal scheduler record that never crosses an external boundary may be clearest as a frozen dataclass or attrs class.
- A payment event API needing strict Python construction, explicit JSON conversion, tagged OpenAPI schemas, finite hostile-input work, and no required runtime dependency graph is the kind of combined tradeoff Talea targets.
- A proprietary binary message with three unusual checks may remain clearer as direct Python, especially if standards projection has no value.
Performance comparisons must use equivalent work: validation cannot be removed from one side, coercion cannot be mistaken for strict acceptance, a native JSON codec cannot be compared to Python-object construction, and error-rich failure paths cannot be compared to boolean predicates without stating the semantic difference.