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.

:)
Python version

Targets Python 3.10–3.14. Python 3.9 and below are End-of-Life.

TABLE OF CONTENTS
5.1namedtuple, NamedTuple or dataclass?

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

NamedTuple and @dataclass both remove record boilerplate. They do not create the same kind of value.

Core answer

Use NamedTuple when tuple behavior is part of the contract. Use @dataclass when the type should behave like a class record with configurable fields, mutability policy, slots, and generated methods.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass
from typing import NamedTuple
class CoordinateTuple(NamedTuple):
x: int
y: int
@dataclass(frozen=True, slots=True)
class CoordinateRecord:
x: int
y: int
def render(point: CoordinateRecord) -> str:
return f"{point.x},{point.y}"
print(CoordinateTuple(3, 5)[0])
print(render(CoordinateRecord(3, 5)))

Why this design exists

Tuple-backed named records preserve positional compatibility with existing tuple APIs. Dataclasses from PEP 557 take a different path: annotations describe fields and the decorator generates normal class methods around those fields.

The design choice is visible to callers. Tuple unpacking, hashing defaults, field factories, inheritance, and later evolution differ.

Mechanics and CPython internals

NamedTuple instances store values as tuple elements. Field names live on the class. Dataclasses inspect annotated class fields and generate methods such as __init__, __repr__, and __eq__; storage is regular instance attributes unless slots=True changes the layout.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass, fields
from typing import NamedTuple
class WirePoint(NamedTuple):
x: int
y: int
@dataclass(slots=True)
class MutablePoint:
x: int
y: int
def describe(point: MutablePoint) -> list[str]:
return [field.name for field in fields(point)]
wire = WirePoint(1, 2)
point = MutablePoint(1, 2)
point.x = 9
print(tuple(wire), describe(point), point)

Complexity and tradeoffs

Both give efficient field access for ordinary code. Tuple-backed records keep positional operations and fixed sequence semantics. Dataclasses trade a slightly broader object model for field configuration, default factories, optional slots, and clearer evolution when position should not be public.

Idiomatic patterns and refactoring

Refactor tuple-position contracts into dataclasses when the record is evolving and index access has become a liability.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class RetryPolicy:
attempts: int
timeout_ms: int
def policy_tuple(raw: tuple[int, int]) -> int:
return raw[0] * raw[1]
def policy_record(policy: RetryPolicy) -> int:
return policy.attempts * policy.timeout_ms
legacy = (3, 250)
modern = RetryPolicy(attempts=3, timeout_ms=250)
print(policy_tuple(legacy))
print(policy_record(modern))

Common mistakes and edge cases

Do not pick NamedTuple if hiding index access matters. Do not assume a dataclass is frozen, hashable, or slotted just because it is concise. Generated methods are public semantics, not comment reduction.

When to use / When NOT to use

Use NamedTuple for tuple-compatible records and dataclasses for class-like domain records.

Do not migrate every tuple blindly; a plain tuple is still appropriate for a small local pair where names add no clarity.

Further reading

  • Official docs: dataclasses
  • Official docs: typing.NamedTuple
  • Official docs: collections.namedtuple
  • PEP 557: data classes
  • CPython source: dataclasses
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