Dataclasses vs NamedTuple, attrs and Pydantic

dataclasses · Expert · 11 min

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) is True. 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=True by 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.

Check yourself

  1. You must validate and coerce JSON from an external API into objects. Which fits best?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/dataclasses.html