frozen=True Dataclasses as Value Objects
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.
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?
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"}
The Output
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
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
tuple or frozenset) for all collection attributes.frozen=True on a dataclass makes all its attributes deeply immutable, including mutable collections like lists, and that frozen objects are automatically hashable.