Python gives you several ways to build record-like objects: namedtuple, NamedTuple, @dataclass. They look similar on the surface but produce fundamentally different runtime shapes. Pick wrong and you lock in the wrong mutability, memory layout, or evolution story.
Think of these as different tools in a machine shop. namedtuple is a shipping label — lightweight, stuck to a tuple underneath, immutable by nature. NamedTuple is the same label but with type annotations printed on it. @dataclass is a full assembly line — you get a real class with all the generated methods, control over mutability, and room to grow.
Use collections.namedtuple for immutable tuple-compatible records without a heavy class body. Use typing.NamedTuple when tuple behavior and annotations both matter. Use @dataclass when you want a normal class with generated methods and more control over mutability and validation.
# [CURRENT - 3.10-3.14] Works on Python 3.xfrom collections import namedtupleCity = namedtuple("City", "name country population")tokyo = City("Tokyo", "JP", 37_000_000)print(tokyo.name)print(tokyo._asdict())# [CURRENT - 3.10-3.14] Works on Python 3.xfrom typing import NamedTupleclass TypedCity(NamedTuple): name: str country: str population: int# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+from dataclasses import dataclass@dataclassclass MutableCity: name: str country: str population: intnamedtuple and NamedTuple instances are tuple subclasses. Their payload is stored in tuple slots, and the field names live on the class, not per instance. That means they preserve tuple semantics:
- indexable
- iterable
- immutable by tuple rules
- hashable if all fields are hashable
Dataclasses create ordinary classes. Without slots=True, their instances typically have an instance __dict__, which means the object body and the attribute mapping are separate allocations.
Measured locally on CPython 3.12.3, 64-bit Linux:
namedtupleinstance with two fields:56bytes- plain dataclass instance with two fields:
48bytes for the object itself - plain dataclass instance
__dict__: about280bytes - slotted dataclass instance with two fields:
48bytes and no__dict__
# [CURRENT - 3.10-3.14] Works on Python 3.10+# Example byte counts below were measured on CPython 3.12.3, 64-bit Linux.import sysfrom collections import namedtuplefrom dataclasses import dataclassPointNT = namedtuple("PointNT", "x y")@dataclassclass PointDC: x: int y: int@dataclass(slots=True)class PointDCS: x: int y: intprint(sys.getsizeof(PointNT(1, 2)))print(sys.getsizeof(PointDC(1, 2)))print(sys.getsizeof(PointDC(1, 2).__dict__))print(sys.getsizeof(PointDCS(1, 2)))That means a non-slotted dataclass can be substantially heavier in real memory than a tuple-based record, even if sys.getsizeof(instance) alone looks small.
All three tools generate useful representation and equality behavior, but they generate different contracts:
- tuple-based builders preserve positional unpacking and tuple ordering semantics
- dataclasses preserve attribute-centric class semantics
# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+from dataclasses import dataclass@dataclass(frozen=True)class Point: x: int y: intprint(Point(1, 2) == Point(1, 2))Annotations still do not validate runtime values by themselves. They inform type checkers and, for dataclasses, field discovery.
collections.namedtuple is long-standing standard library. typing.NamedTuple is modern Python 3 typing support. Dataclasses arrived in Python 3.7 via PEP 557. Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.
Tuple compatibility is a strong API promise. If callers unpack or index your NamedTuple, field reordering becomes a breaking change. Dataclasses are easier to evolve when callers use attribute names.
namedtuple and NamedTuple also encourage positional semantics. That is efficient, but it can become opaque for wide records.
A field-only dataclass can still be a bad design if the invariants live elsewhere. Generated methods remove boilerplate; they do not remove the need for domain behavior and validation.
Use this decision rule:
- tuple compatibility required:
namedtupleorNamedTuple - typed immutable record:
NamedTupleor@dataclass(frozen=True) - many small instances with attribute access: consider
@dataclass(slots=True)after measuring - mutable domain object with validation hooks:
@dataclass
# [OLDER / 3.7-3.8, CURRENT - 3.10-3.14] Works on Python 3.7+from dataclasses import dataclass@dataclassclass Email: value: str def __post_init__(self): if "@" not in self.value: raise ValueError("invalid email")For field-level control, see . For annotation semantics, see .