Back to Blog
Python

Python Dataclass Frozen: Making Data Immutable

python dataclass frozen: Learn how to create immutable data structures with frozen dataclasses in Python, including syntax, behavior, and practical use cases.

dataclassesimmutabilityPythonobject-oriented programmingtype hints
A stylized padlock integrated with a Python code snippet, representing immutable frozen dataclasses.

python dataclass frozen requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you need an object whose attributes cannot change after creation, Python's dataclass decorator offers a straightforward solution through the frozen parameter. Setting frozen=True generates a class that raises FrozenInstanceError if you attempt to assign a new value to any field after instantiation. This behavior is essential for value objects, configuration records, and any data that must remain consistent across threads or function boundaries.

What frozen=True Actually Does

A frozen dataclass is not a separate type; it is a regular dataclass with generated __setattr__ and __delattr__ methods that prevent modification. When you write:

from dataclasses import dataclass @dataclass(frozen=True) class Point: x: float y: float

the generated class overrides attribute assignment so that any attempt to set p.x = 3 raises dataclasses.FrozenInstanceError. The same applies to deleting attributes. Internally, Python uses object.__setattr__ to set the initial values during __init__, which is why construction still works.

This immutability is shallow. If a field contains a mutable object, such as a list or dictionary, you can still modify that object's contents. The reference itself is protected, but the object it points to is not. For deep immutability, you need to use immutable field types like tuple or frozenset, or rely on libraries that provide recursive immutability.

Creating a Frozen Dataclass

The syntax is minimal. Add frozen=True to the decorator and define fields as usual:

@dataclass(frozen=True) class User: id: int name: str email: str

Instantiation works exactly like a normal dataclass:

u = User(id=1, name="Alice", email="alice@example.com") print(u.name) # Alice

Attempting to modify a field produces an error:

try: u.name = "Bob" except dataclasses.FrozenInstanceError as e: print("Cannot modify frozen instance")

The exception is a subclass of AttributeError, so code that catches AttributeError will also catch this case. This behavior is consistent across Python 3.7 and later, where dataclasses were introduced.

Why Use a Frozen Dataclass?

Immutable objects simplify reasoning about state. Once created, you never have to worry about accidental changes from other parts of the code. This is particularly useful for:

  • Configuration values that are loaded once and must not change during runtime.
  • Value objects in domain-driven design, where equality is based on field values rather than identity.
  • Keys in dictionaries or sets, because immutable objects have a stable hash if all fields are hashable.

Frozen dataclasses also work well in concurrent code. Since no thread can mutate the object, you avoid races without adding locks. The immutability is a compile-time guarantee that carries into runtime behavior.

Hashability and Equality

By default, dataclasses are not hashable because they define __eq__ but not __hash__. Setting frozen=True changes this: the generated class sets __hash__ to the hash of a tuple of field values. This means frozen dataclasses can be used as dictionary keys or set members, provided all fields are hashable.

@dataclass(frozen=True) class Coordinate: lat: float lon: float locations = {Coordinate(40.7, -74.0): "NYC"} print(locations[Coordinate(40.7, -74.0)]) # NYC

If a field is unhashable, such as a list, the hash will fail at runtime. You can override __hash__ manually, but doing so breaks the immutability contract if you rely on mutable state.

Modifying a Frozen Dataclass: dataclasses.replace

Immutable doesn't mean you can never change values. The dataclasses.replace function creates a new instance with specified fields replaced, leaving the original untouched. This is the idiomatic way to "update" a frozen object.

from dataclasses import replace original = User(id=1, name="Alice", email="alice@example.com") updated = replace(original, email="alice@newdomain.com") print(original.email) # alice@example.com print(updated.email) # alice@newdomain.com

replace copies all fields and then applies the changes. It works with any dataclass, but is especially important for frozen ones because direct assignment is impossible. This pattern encourages a functional style where state changes produce new objects rather than mutating existing ones.

Performance and Memory Considerations

Frozen dataclasses have a small runtime overhead compared to plain dataclasses. The generated __setattr__ and __delattr__ methods add an extra layer of indirection for attribute assignment. However, attribute reads are unaffected because they use the normal __getattribute__ path. In practice, the performance difference is negligible for most applications, but if you are doing millions of attribute assignments in a hot loop, a regular dataclass will be slightly faster.

Memory usage is identical to a normal dataclass because the object structure is the same. The immutability does not add any per-instance storage. The hash, when computed, is recalculated each time you call hash() on the object, which may be a consideration if you use frozen dataclasses heavily as dictionary keys. Python does not cache the hash for user-defined objects unless you implement __hash__ with caching.

When a Frozen Dataclass Is Not Enough

Frozen dataclasses prevent attribute assignment, but they do not prevent mutation of nested mutable objects. Consider:

@dataclass(frozen=True) class Order: items: list order = Order(items=["apple", "banana"]) order.items.append("cherry") # This works!

The items list is mutable, so you can change its contents even though order is frozen. To achieve true immutability, you must use immutable collection types or copy-on-write patterns. For example, store a tuple instead of a list:

@dataclass(frozen=True) class Order: items: tuple

Now order.items cannot be modified because tuples are immutable. For nested structures, you may need to recursively convert lists to tuples and dicts to MappingProxyType or use a library like immutables.

Another limitation is that frozen dataclasses do not prevent calling methods that mutate internal state. If you define a method that modifies a list field, the immutability of the dataclass does not stop it. You must design the class to avoid such methods.

Compatibility and Version Notes

Dataclasses were introduced in Python 3.7. The frozen parameter has been available since that version and has not changed in behavior. In Python 3.10 and later, dataclasses also supports slots=True, which can be combined with frozen=True to reduce memory usage. Slotted classes do not have a __dict__, which makes attribute assignment even more restricted, but the immutability guarantee remains the same.

@dataclass(frozen=True, slots=True) class Config: host: str port: int

When using slots=True, the generated __setattr__ still raises FrozenInstanceError on assignment. The combination is useful for high-performance applications that create many instances.

If you need to support Python 3.6, you must install the dataclasses backport package. The frozen parameter behaves identically in that backport, but you should verify compatibility with your tooling.

Choosing Between Frozen and Regular Dataclasses

The decision depends on how you intend to use the object. If the object represents a mutable entity that changes over time, such as a user session or a shopping cart, a regular dataclass is appropriate. If the object is a value that should remain constant, like a point in a graph or a database connection string, a frozen dataclass provides safety and enables hashability.

A practical rule: use frozen=True unless you have a specific reason to mutate the object. Immutability reduces bugs and makes code easier to reason about. When you do need to change a value, replace gives you a clean way to create a new instance. This pattern aligns with functional programming principles and works well in large codebases where shared objects are common.

python dataclass frozen: Practical Usage and Code Examples | RYUSLOG DEV