Picking the right record type affects validation, performance, immutability and API ergonomics.
Python has several ways to define record-like types:
- dataclass: standard library, mutable or frozen, methods and inheritance, no runtime validation. The default choice for internal domain objects.
- `typing.NamedTuple`: an immutable tuple subclass with named fields. Supports unpacking and indexing, memory-light, but tuple semantics leak:
Point(1, 2) == (1, 2)isTrue. Good for small, positional return values. - `TypedDict`: types plain dicts; no class instances at all. Good for JSON-shaped data that stays a dict.
- attrs: the third-party library that inspired dataclasses; adds validators, converters, and more options, with
slots=Trueby default in its modern API. - Pydantic: runtime validation and parsing from untrusted input (JSON, env vars), coercion, serialisation and JSON Schema. Heavier, but the right tool at system boundaries.
A common architecture: Pydantic models at the edges (API requests, config), plain dataclasses inside.
How do type checkers understand attrs, Pydantic, SQLAlchemy models and others? typing.dataclass_transform() (PEP 681, 3.11) marks a decorator, base class or metaclass as "creates dataclass-like classes", so mypy and pyright synthesise the right __init__ signature without plugins.
Example
from dataclasses import dataclass
from typing import NamedTuple, TypedDict, dataclass_transform
@dataclass(frozen=True, slots=True)
class PointDC:
x: int; y: int
class PointNT(NamedTuple):
x: int; y: int
class PointTD(TypedDict):
x: int; y: int
print(PointDC(1, 2) == (1, 2), PointNT(1, 2) == (1, 2)) # False True
x, y = PointNT(3, 4) # tuple unpacking works
print(x, PointTD(x=1, y=2)) # 3 {'x': 1, 'y': 2}
@dataclass_transform()
def model(cls): # checkers now treat @model classes like dataclasses
return dataclass(cls)
@model
class User:
name: str
print(User(name="ada")) # User(name='ada')Remember
- Dataclasses for internal objects; Pydantic for validating untrusted input
- NamedTuple is a tuple: it equals plain tuples and supports unpacking
- dataclass_transform teaches type checkers about dataclass-like libraries
Watch out for
- Using NamedTuple for domain objects and getting surprised that they compare equal to unrelated tuples.