namedtuple, NamedTuple or dataclass?

Pick the right data class builder for records, typed tuples, and mutable objects

`namedtuple`, `typing.NamedTuple`, and `@dataclass` all build records. Each makes different tradeoffs. `namedtuple` creates a tuple subclass with named fields and tuple semantics: immutable, iterable, indexable, and hashable if all fields are hashable. `typing.NamedTuple` adds field annotations to the same tuple subclass. `@dataclass` creates an ordinary class with generated `__init__`, `__repr__`, `__eq__`, and optionally `__order__` and `__hash__`. Without `slots=True`, dataclasses store fields in `__dict__` like regular instances. Tuple subclasses use less memory per instance because they inherit the compact inline layout. This page covers memory measurements, field access patterns, and which builder fits each situation. <a href="/classes-dataclass-fields">Explore dataclass fields configuration in depth</a>. <a href="/memory-tuples-lists">See how namedtuple shares tuple memory characteristics</a>.

Understand.
Visualize.
Master.

Python in Depth

An interactive engineering reference for Python internals

Quick note

Generated convenience still defines API shape.

:)
TABLE OF CONTENTS
5.1namedtuple, NamedTuple or dataclass?

Pick the right data class builder for records, typed tuples, and mutable objects

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.

Core answer

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.x
from collections import namedtuple
City = 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.x
from typing import NamedTuple
class 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
@dataclass
class MutableCity:
name: str
country: str
population: int
Mechanism and object layout

namedtuple 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:

  • namedtuple instance with two fields: 56 bytes
  • plain dataclass instance with two fields: 48 bytes for the object itself
  • plain dataclass instance __dict__: about 280 bytes
  • slotted dataclass instance with two fields: 48 bytes 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 sys
from collections import namedtuple
from dataclasses import dataclass
PointNT = namedtuple("PointNT", "x y")
@dataclass
class PointDC:
x: int
y: int
@dataclass(slots=True)
class PointDCS:
x: int
y: int
print(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.

Generated behavior

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: int
print(Point(1, 2) == Point(1, 2))

Annotations still do not validate runtime values by themselves. They inform type checkers and, for dataclasses, field discovery.

Version context

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.

Edge cases and gotchas

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.

Production usage

Use this decision rule:

  • tuple compatibility required: namedtuple or NamedTuple
  • typed immutable record: NamedTuple or @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
@dataclass
class 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 .

Further depth
  • collections.namedtuple
  • typing.NamedTuple
  • dataclasses module
  • PEP 557: Data Classes
  • sys.getsizeof
BOARD NOTESContext
WHY NO BENCHMARK?

This topic is better taught with structure, semantics, and cross-references than with a synthetic chart.

Generated convenience still defines API shape.

RELATED GUIDES
NEXT CHECKS
Contribute