Python Dataclasses: Immutable Value Objects with Frozen Instances, Default Factories, and Slots
A practical guide to using frozen dataclasses, default_factory for mutable defaults, and slots to cut memory and bugs in Python 3.7+ codebases.
07 Oct 2026, 20:38 UTC

Problem: Shared mutable defaults cause subtle bugs
When you define a dataclass field with a mutable default such as list or dict, every instance shares the same object unless you use default_factory. This leads to unexpected state leakage and makes reasoning about equality harder.
Thesis
Python’s @dataclass decorator lets you declare a value object in a single line. By combining frozen=True, default_factory, and slots=True (Python 3.10+), you get an immutable, correctly shared, and memory‑efficient class without writing boilerplate.
1. Dataclass basics
The decorator inspects annotated fields and automatically creates __init__, __repr__, __eq__, and ordering methods. Example:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
Running this in any Python 3.7+ interpreter creates a class that expects x and y at construction and provides sensible representation and equality.
2. Making instances immutable
Set frozen=True to freeze the object after __init__ finishes. Any attempt to assign a new value raises FrozenInstanceError, which is useful for safe sharing and for using the instance as a dictionary key.
from dataclasses import dataclass, FrozenInstanceError
@dataclass(frozen=True)
class ImmutablePoint:
x: int
y: int
p = ImmutablePoint(1, 2)
# p.x = 3 # raises FrozenInstanceError
Verification step: try to mutate an attribute and confirm the exception is raised.
3. Safe mutable defaults with default_factory
Mutable defaults are evaluated once at class definition time. To avoid shared state, supply a factory function via field(default_factory=…). Example:
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Config:
tags: list = field(default_factory=list)
settings: dict = field(default_factory=dict)
c1 = Config()
c2 = Config()
c1.tags.append('alpha')
print(c2.tags) # [] – each instance gets its own list
Risk: forgetting default_factory leads to all instances sharing the same list or dict, a classic source of bugs.
4. Reducing memory overhead with slots
Starting in Python 3.10, add slots=True to remove the per‑instance __dict__. This cuts memory usage and speeds attribute access, which matters when you have many instances.
from dataclasses import dataclass, field
@dataclass(slots=True, frozen=True)
class SmallRecord:
id: int
name: str
tags: list = field(default_factory=list)
Verification: inspect dataclasses.fields(SmallRecord) and check that __slots__ appears in the class dict.
5. Trade‑off: immutability changes usage patterns
Because a frozen dataclass cannot be updated in place, you must create a new instance for any logical “update”. This can be more verbose but also makes reasoning about state easier and enables safe sharing across threads.
Performance impact is usually positive: less memory, faster attribute lookup, and hashability (if all fields are immutable).
6. Concrete worked example
Let’s build a small value object that represents a user configuration. It must be immutable, support default mutable collections, and use slots for efficiency.
from dataclasses import dataclass, field
from typing import List, Dict
@dataclass(slots=True, frozen=True)
class UserConfig:
user_id: int
preferences: Dict[str, str] = field(default_factory=dict)
tags: List[str] = field(default_factory=list)
# Verification
c1 = UserConfig(42, {'theme': 'dark'})
c2 = UserConfig(42, {'theme': 'light'})
# Equality and hash
assert c1 == c2 is False
assert hash(c1) == hash(c2) # works because frozen and all fields comparable
# Attempt mutation – should fail
# c1.preferences['new'] = 'value' # raises FrozenInstanceError
Run the snippet in a Python 3.10+ interpreter. Check that the exception occurs on mutation and that two instances with identical data compare equal and have the same hash.
7. When to use this pattern
- Value objects that represent a fixed set of attributes (e.g., coordinates, configuration snapshots).
- Scenarios where multiple components need to share the same data without risk of accidental mutation.
- High‑throughput codebases where memory footprint matters.
8. Closing action steps
- Identify a class that is essentially a container of data.
- Add
@dataclass(frozen=True, slots=True)if you are on Python 3.10+; otherwise usefrozen=Truealone. - Replace any mutable default arguments with
field(default_factory=…). - Run the verification steps: try to assign a new value, compare two identical instances, and inspect
dataclasses.fieldsfor generated slots.
By following these steps you gain a concise, correct, and memory‑friendly definition of immutable value objects in Python.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.