Frozen Dataclass vs NamedTuple in Python: Which to Choose?
Frozen dataclasses and namedtuples are both useful immutable data containers in Python. This article compares their syntax, immutability, type hints, defaults, methods, performance, and the best situations for each.
The Core Decision: Frozen Dataclass or NamedTuple?
When you need an immutable data container in Python, the choice often comes down to a frozen dataclass or a namedtuple. Both give you a lightweight object with named fields, but they differ in syntax, type handling, and runtime behavior. This article compares them directly so you can pick the right one for your codebase.
Syntax and Declaration Differences
A namedtuple is created with a factory function:
from collections import namedtuple Point = namedtuple("Point", ["x", "y"]) p = Point(1, 2)
A frozen dataclass uses a class definition with the @dataclass(frozen=True) decorator:
from dataclasses import dataclass @dataclass(frozen=True) class Point: x: int y: int p = Point(1, 2)
The dataclass version is more explicit about types and reads like a regular class. The namedtuple is more concise but relies on string field names.
Immutability Behavior and Nested Mutation
Both types prevent reassigning attributes. Attempting p.x = 3 raises AttributeError for both. However, immutability is shallow. If a field holds a mutable object like a list, you can still modify that list:
@dataclass(frozen=True) class Container: items: list c = Container([1, 2, 3]) c.items.append(4) # works
The same happens with namedtuple. Neither provides deep immutability. If you need that, you must store immutable types or use a custom implementation.
Type Hints and Field Definitions
Dataclasses require type annotations for each field. This gives you static type checking with tools like mypy and better editor autocompletion. Namedtuples do not enforce types; they are just tuples with named access. You can add type hints to a namedtuple using typing.NamedTuple, but that is a separate class-based syntax:
from typing import NamedTuple class Point(NamedTuple): x: int y: int
This is closer to a dataclass but still lacks features like field metadata and __post_init__.
Default Values and Field Options
Dataclasses support default values, default_factory for mutable defaults, and field() for metadata:
from dataclasses import dataclass, field @dataclass(frozen=True) class Config: name: str retries: int = 3 tags: list = field(default_factory=list)
The collections.namedtuple factory gained a defaults parameter in Python 3.7; defaults apply to the rightmost fields, and there is no factory equivalent to default_factory:
Point = namedtuple("Point", ["x", "y"], defaults=[0])
The typing.NamedTuple variant also supports defaults via class attribute assignment, but it still does not provide a factory for mutable defaults. If you need per-field metadata or complex default logic, a dataclass is more flexible.
Methods and Extensibility
Dataclasses are ordinary classes, so you can define methods directly:
@dataclass(frozen=True) class Rectangle: width: float height: float def area(self) -> float: return self.width * self.height
Namedtuples are tuple subclasses. You can add methods by subclassing, but the syntax is more awkward and you must override __new__ if you want custom initialization. Dataclasses also support __post_init__ for validation, which namedtuples lack.
Performance and Memory Considerations
Namedtuples are built on tuples, so they are memory efficient and fast to create. Dataclasses, by default, store each instance in a __dict__, which uses more memory and has slower attribute access. In Python 3.10 and later, you can mitigate this by adding slots=True to the dataclass decorator:
@dataclass(frozen=True, slots=True) class Point: x: int y: int
This removes __dict__ and reduces memory usage, bringing it closer to a namedtuple. Slots also prevent dynamic attributes, so you cannot add new attributes to an instance. For high-frequency object creation in tight loops, a namedtuple may still be faster, but the difference is often negligible unless you measure it in your specific workload.
When to Use Which
Choose a frozen dataclass when you need type hints, field defaults, validation, or methods. It is a better fit for domain models and configuration objects where readability and maintainability matter more than raw performance.
Choose a namedtuple when you need a simple, lightweight immutable record with minimal ceremony. It is useful for internal data passing, return values from functions, or when you want tuple unpacking and comparison behavior out of the box.
If your codebase already uses dataclasses, adding a frozen dataclass is more consistent. If you are optimizing memory for millions of small objects, a namedtuple or a slots-based dataclass is worth considering.