Python
Immutable Object
Programming
Python Tips
Object-Oriented Programming

How to make an immutable object in Python?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Python does not have a keyword that turns an arbitrary custom class into a perfectly immutable object. Instead, immutability is usually a design choice: restrict attribute mutation, choose immutable field types, and avoid exposing internal mutable state.

For most modern Python code, the best starting point is a frozen dataclass. It is concise, readable, and communicates intent clearly.

Use A Frozen Dataclass

A frozen dataclass prevents attribute reassignment after construction:

python
1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class Point:
5    x: int
6    y: int
7
8p = Point(2, 3)
9print(p.x)

Trying to assign p.x = 10 raises an exception. This gives you practical immutability for the object interface.

Immutability Depends On The Fields Too

A frozen outer object is not truly immutable if it contains mutable fields:

python
1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class BadConfig:
5    values: list

You cannot rebind values, but you can still mutate the list itself. A better design uses immutable field types such as tuples or frozensets:

python
1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class GoodConfig:
5    values: tuple[str, ...]

That is the real rule: immutability is only as strong as the mutability of the contained data.

Alternative: Custom __setattr__

Before dataclasses became the default answer, developers often blocked assignment manually:

python
1class FrozenPoint:
2    __slots__ = ("x", "y", "_initialized")
3
4    def __init__(self, x, y):
5        object.__setattr__(self, "x", x)
6        object.__setattr__(self, "y", y)
7        object.__setattr__(self, "_initialized", True)
8
9    def __setattr__(self, name, value):
10        if getattr(self, "_initialized", False):
11            raise AttributeError("immutable object")
12        object.__setattr__(self, name, value)

This works, but it is more code and easier to get wrong than a frozen dataclass.

Why Immutability Helps

Immutable objects are easier to reason about because their state does not drift after creation. They are often safer to share across functions and threads, and they are good candidates for dictionary keys when their fields are also hashable.

The main tradeoff is that updates become replace-instead-of-modify operations. That is usually a good trade in value-object style code.

Named Tuples And Value Objects

Not every immutable object needs a custom class. For lightweight value objects, tuples, NamedTuple, and frozen dataclasses often cover most needs with far less code than a hand-written class hierarchy. Choosing the smallest construct that expresses the data usually makes immutability easier to maintain.

Construction Time Is Your Mutation Window

Immutable-object patterns usually concentrate all allowed mutation into object creation. After __init__ or dataclass construction finishes, the public state should stop changing. Thinking in terms of “build once, then freeze” helps keep the design consistent.

Hashability Often Follows Immutability

Value objects are especially useful when you want instances to behave well as dictionary keys or set members.

That makes immutable instances easier to reuse confidently.

It also reduces accidental mutation bugs in larger codebases.

Common Pitfalls

  • Freezing the object while storing mutable fields such as lists or dictionaries inside it.
  • Assuming __slots__ alone makes an object immutable.
  • Writing a custom __setattr__ solution when a frozen dataclass would be clearer.
  • Exposing internal mutable objects through methods or properties.
  • Confusing practical immutability with absolute tamper-proofing in Python.

Summary

  • In modern Python, a frozen dataclass is usually the best way to model an immutable object.
  • Immutability depends on both the object and the types of its fields.
  • Prefer tuples, frozensets, and other immutable containers for stored state.
  • Custom __setattr__ patterns still work, but they are more error-prone.
  • Think of immutability as a design discipline, not just a decorator flag.

Course illustration
Course illustration