← Python Code Modern OOP
Browse Python Concepts

frozen=True Dataclasses as Value Objects

Mental Model

Think of frozen=True as applying a superficial layer of ice to the dataclass object itself, preventing direct reassignments to its attributes. However, if an attribute holds a reference to a mutable object (like a list), that object underneath the ice layer can still be changed. Hashes depend on immutability.

Rule: When defining frozen dataclasses as value objects, always use strictly immutable types (like tuple or frozenset) for all collection attributes.

The Setup

You are building a configuration manager that caches connection properties. You decide to make the config class a frozen dataclass to make it hashable and safely cacheable in a dictionary. One of the configuration keys contains a list of fallback IP addresses.

What Does This Print?

Broken code
Python
from dataclasses import dataclass

@dataclass(frozen=True)
class CacheConfig:
    host: str
    fallback_ips: list[str]

config = CacheConfig(host="localhost", fallback_ips=["10.0.0.1", "10.0.0.2"])
# Try to mutate a 'frozen' configuration item
config.fallback_ips.append("10.0.0.3")
# Try to hash the config for a cache lookup
config_cache = {config: "active_status"}
What happens when you run this code? Does appending throw an error, or does the hashing step fail?

The Output

What actually happens
TypeError: unhashable type: 'list'

The mutation of the inner list succeeds completely without any error. However, when trying to use the object as a dictionary key, Python raises a runtime TypeError: unhashable type: 'list'. While the CacheConfig instance itself is marked frozen, the object properties referencing the list are mutable, which makes the whole object unhashable.

Why Python Does This

In Python, frozen=True only blocks direct assignments on the dataclass instance itself (by overriding __setattr__ and __delattr__ to raise FrozenInstanceError). It does not perform deep copies or recursively freeze objects stored in its attributes. If an attribute contains a pointer to a mutable object (like a list), you can still modify that object in-place. Additionally, Python generates a __hash__ method for frozen dataclasses only if all fields have valid hash methods. Since list has no hash method, the generated __hash__ call fails.

The Fix

Corrected pattern
Python
from dataclasses import dataclass
from typing import Tuple

@dataclass(frozen=True)
class CacheConfig:
    host: str
    # By utilizing an immutable tuple instead of a list, the class is truly immutable and hashable
    fallback_ips: Tuple[str, ...]

config = CacheConfig(host="localhost", fallback_ips=("10.0.0.1", "10.0.0.2"))
config_cache = {config: "active_status"}  # Works perfectly

Using immutable types like tuple or frozenset for collection attributes ensures that the contents of those collections cannot be modified after instantiation. This guarantees true immutability, which is a prerequisite for an object to be reliably hashable and suitable for use as a dictionary key or in a set.

How This Fails in Real Systems

A high-frequency configuration system used frozen dataclasses to key in-memory state objects. Over time, an internal monitoring module directly appended elements to lists inside the configuration. The hash calculation was stripped, throwing unhandled tracebacks in the core microservice router and resulting in a 2-hour production outage.

Key Takeaway

When defining frozen dataclasses as value objects, always use strictly immutable types (like tuple or frozenset) for all collection attributes.
Common mistake: Developers assume that frozen=True on a dataclass makes all its attributes deeply immutable, including mutable collections like lists, and that frozen objects are automatically hashable.