StackKit

Python dataclass vs NamedTuple vs TypedDict: Choosing a Data Container

By · · 4 min read

Python gives you several ways to group related values: plain dictionaries, tuples, classes, and the typed helpers in the standard library. Three of those helpers are easy to confuse: dataclasses.dataclass, typing.NamedTuple and typing.TypedDict. All three let you describe a record with named, typed fields. They behave very differently at runtime.

The examples below were run on Python 3.15. The features used need Python 3.10 or newer (slots=True), and NotRequired needs 3.11.

The short version

dataclass NamedTuple TypedDict
Runtime type regular class tuple subclass plain dict
Mutable yes (unless frozen=True) no yes
Unpackable like a tuple no yes no
Defaults, methods, validation yes defaults and methods no
Checked at runtime only what you code arity only not at all
Best for domain objects small immutable records JSON-shaped data

dataclass: a normal class without the boilerplate

@dataclass generates __init__, __repr__ and __eq__ from the annotated fields.

from dataclasses import dataclass, field, asdict

@dataclass(frozen=True, slots=True)
class Item:
    sku: str
    qty: int = 1
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.qty < 1:
            raise ValueError("qty must be >= 1")

item = Item("A1", 2, ["x"])
print(item)           # Item(sku='A1', qty=2, tags=['x'])
print(asdict(item))   # {'sku': 'A1', 'qty': 2, 'tags': ['x']}
print(Item("A1", 2, ["x"]) == item)  # True

Points worth knowing:

  • Mutable defaults need default_factory. Writing tags: list[str] = [] raises an error, which protects you from the shared-list bug.
  • frozen=True makes instances immutable; assigning to a field raises FrozenInstanceError. Frozen dataclasses with hashable fields can also be dictionary keys.
  • slots=True (Python 3.10+) uses __slots__, which saves memory and blocks accidental new attributes.
  • __post_init__ is the place for validation, as shown. Without it, a dataclass does not check types or values.
try:
    Item("B", 0)
except ValueError as e:
    print(e)   # qty must be >= 1

Choose a dataclass when the data has behaviour, needs validation, or will be changed over time.

NamedTuple: an immutable record you can unpack

NamedTuple creates a real tuple whose positions also have names.

from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int

p = Point(1, 2)
x, y = p
print(p, x, y, p[0])   # Point(x=1, y=2) 1 2 1

Because it is a tuple, a NamedTuple is immutable, unpackable, indexable, hashable (if its values are), and very light. It is a good return type for functions that hand back a few related values, replacing a bare (a, b, c) whose meaning nobody remembers.

The trade-off is that tuple behaviour leaks through. Two different NamedTuples with the same values compare equal to each other, and to a plain tuple, which can surprise you:

class Size(NamedTuple):
    w: int
    h: int

print(Point(1, 2) == Size(1, 2))  # True

If that matters, use a dataclass.

TypedDict: a type hint for a dictionary

TypedDict is different in kind. It creates no new type at runtime. Instances are ordinary dictionaries; the class only tells a type checker which keys exist and what their values should be.

from typing import TypedDict

class User(TypedDict):
    id: int
    name: str

u: User = {"id": 1, "name": "ada"}
print(u, type(u))   # {'id': 1, 'name': 'ada'} <class 'dict'>

That makes it ideal for data you receive as a dict and want to describe: parsed JSON, API responses, configuration. You get autocomplete and static checking in an editor or with mypy, and you pay no conversion cost.

Remember the limits: Python itself will not stop you from building a User with a missing key or a wrong type. The check happens only in your type checker. If you need validation of external data at runtime, parse it into a dataclass yourself or use a validation library such as Pydantic.

Optional keys can be marked with NotRequired[...] (Python 3.11+) or by setting total=False on the class.

How to choose

  • It has methods, defaults or validation, or it will change: dataclass.
  • It is a small, fixed, immutable group of values, perhaps returned from a function: NamedTuple.
  • It is already a dictionary (JSON, config) and you want static checking only: TypedDict.
  • You need runtime validation of untrusted input: write checks in __post_init__, or use a validation library.

Common mistakes

  • Using TypedDict and expecting runtime errors for bad data.
  • Using a mutable default value in a dataclass instead of default_factory.
  • Using a NamedTuple for something that later needs methods or validation, then fighting tuple semantics.
  • Comparing NamedTuples of different classes and trusting the result.

Summary

All three describe a record, but they live at different levels: a dataclass is a full class, a NamedTuple is a tuple with names, and a TypedDict is only a promise to the type checker. Start from what you need at runtime, and the right one is usually obvious.