Python Frozen Dataclasses and Pseudo-Immutability
Python's dataclasses module allows developers to create
structures that behave like immutable records by passing
frozen=True to the @dataclass decorator. This
article explains how frozen dataclasses enforce read-only behavior, why
this protection is classified as pseudo-immutability rather than
absolute immutability, and the common ways this boundary can be bypassed
or weakened.
How Frozen Dataclasses Work
When you define a dataclass with
@dataclass(frozen=True), Python automatically generates
special implementations for attribute assignment and deletion:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: intUnder the hood, the decorator injects __setattr__() and
__delattr__() methods into the generated class. Whenever
code attempts to modify an attribute (e.g., point.x = 10)
or delete one (e.g., del point.x), these generated methods
raise a dataclasses.FrozenInstanceError. Additionally,
frozen dataclasses automatically generate a __hash__()
method by default, allowing instances to be used as dictionary keys or
set elements, provided all defined fields are also hashable.
Why It Is "Pseudo-Immutability"
Python is fundamentally a dynamic language that relies on convention ("we are all consenting adults here") rather than hard runtime memory protections. Because of this architecture, frozen dataclasses provide pseudo-immutability rather than true, hardware- or runtime-enforced immutability for two primary reasons:
1. Mutable Nested Objects
The "frozen" constraint only prevents reassignment of the top-level references held by the dataclass; it does not freeze the underlying objects themselves. If a field references a mutable object, that object can still be modified in place.
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class Team:
members: List[str]
team = Team(members=["Alice", "Bob"])
# Reassignment fails:
# team.members = ["Charlie"] -> Raises FrozenInstanceError
# Mutation succeeds:
team.members.append("Charlie") # Modifies the internal listIn this scenario, team has changed state despite being
marked as frozen. To achieve deeper immutability, mutable collections
like list or dict must be replaced with
immutable alternatives such as tuple or
frozenset.
2. Direct Bypass via
object.__setattr__
Because the freezing mechanism relies entirely on Python-level method
interception, it can be bypassed directly using the base
object methods.
point = Point(1, 2)
object.__setattr__(point, 'x', 10)
print(point.x) # Outputs: 10By calling object.__setattr__, the dataclass's custom
__setattr__ check is completely circumvented. In fact,
Python's own dataclass implementation relies on this exact mechanism
during __init__ to populate the initial values without
raising a FrozenInstanceError.
Summary
Frozen dataclasses provide defensive programming safeguards to prevent accidental reassignments in standard application workflows. However, because they do not protect nested mutable structures and can be bypassed via dynamic introspection, their immutability remains superficial—a pragmatic convention rather than an unbreakable guarantee.