Python dataclass vs NamedTuple vs TypedDict: Choosing a Data Container
By Nitheesh Rajendran · · 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. Writingtags: list[str] = []raises an error, which protects you from the shared-list bug. frozen=Truemakes instances immutable; assigning to a field raisesFrozenInstanceError. 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
TypedDictand expecting runtime errors for bad data. - Using a mutable default value in a dataclass instead of
default_factory. - Using a
NamedTuplefor 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.