Advertisement
🚀 Advanced Python

Python Dataclasses – Clean Data Container Classes

Dataclasses (Python 3.7+) automatically generate boilerplate methods (__init__, __repr__, __eq__) based on type-annotated class variables. They dramatically reduce the code needed to create data-holding classes while maintaining all the power of regular classes.

⏱️ 18 min read🎯 Advanced📅 Updated 2026

Basic Dataclass

Decorate a class with @dataclass and annotate fields. Python generates __init__, __repr__, and __eq__ automatically.

Python
from dataclasses import dataclass

# Without @dataclass (verbose)
class PersonManual:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def __repr__(self):
        return f"Person(name={self.name!r}, age={self.age!r})"

# With @dataclass (clean!)
@dataclass
class Person:
    name: str
    age: int

p = Person("Alice", 30)
print(p)            # Person(name='Alice', age=30)
print(p.name)       # Alice
print(Person("Bob", 25) == Person("Bob", 25))  # True
▶ Output
Person(name='Alice', age=30) Alice True

Default Values and field()

Use field() for mutable defaults (lists, dicts). Never use mutable defaults directly.

Python
from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    grade: float = 0.0
    courses: list = field(default_factory=list)  # Correct!
    # courses: list = []  ← WRONG — shared across instances!

s1 = Student("Alice")
s2 = Student("Bob")

s1.courses.append("Python")
print(s1.courses)  # ['Python']
print(s2.courses)  # []  — independent list!
▶ Output
['Python'] []

__post_init__ – Computed Fields

Run code after __init__ for validation or computed attributes.

Python
from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)  # Not a constructor param

    def __post_init__(self):
        if self.width <= 0 or self.height <= 0:
            raise ValueError("Dimensions must be positive")
        self.area = self.width * self.height

r = Rectangle(5, 3)
print(r)        # Rectangle(width=5, height=3, area=15)
print(r.area)   # 15
▶ Output
Rectangle(width=5, height=3, area=15) 15
Advertisement

Frozen Dataclasses – Immutable Objects

frozen=True makes all fields read-only and the object hashable (usable as dict key).

Python
from dataclasses import dataclass

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

p = Point(3.0, 4.0)
print(p)          # Point(x=3.0, y=4.0)

try:
    p.x = 10     # AttributeError!
except Exception as e:
    print(e)

# Hashable — can be dict key or set member
locations = {p: "Home"}
print(locations[Point(3.0, 4.0)])  # Home
▶ Output
Point(x=3.0, y=4.0) cannot assign to field 'x' Home

@dataclass: Kill the Boilerplate

A @dataclass auto-generates __init__, __repr__, and __eq__ from your field annotations. It turns a 15-line class into 4 — the pythonic way to write a "just holds data" class.

from dataclasses import dataclass, field

@dataclass
class Point:
    x: int
    y: int = 0                       # default value
    tags: list = field(default_factory=list)   # ⚠️ NOT tags=[]

p = Point(1, 2)
print(p)              # Point(x=1, y=2, tags=[])  ← free __repr__
Point(1, 2) == Point(1, 2)   # True — free __eq__

The mutable-default trap (again): you can't write tags: list = [] — that shares one list across all instances, and dataclasses actually raise an error to stop you. Use field(default_factory=list) to build a fresh one per instance.

OptionEffect
@dataclass(frozen=True)immutable + hashable (usable as dict key)
@dataclass(order=True)adds <, > comparisons
field(repr=False)hide a field from __repr__

Reach for a dataclass over a plain class when the object is mostly fields; over a NamedTuple when you need mutability or methods.

🏋️ Practical Exercise

Use the @dataclass decorator:

  1. Create a Point dataclass with x and y fields and print an instance.
  2. Add a default value to one field and a list field using field(default_factory=list).
  3. Add a __post_init__ that computes a derived field.
  4. Create a frozen=True dataclass and confirm its instances are immutable.

🔥 Challenge Exercise

Model an Order dataclass with a customer name, a list of line items, and a computed total filled in by __post_init__. Use field(default_factory=list) for the items so each order gets its own list. Make a frozen Money dataclass and use it inside the order to demonstrate immutability and automatic __eq__. Bonus: sort a list of orders by total using the auto-generated comparison via order=True.

📋 Summary

  • The @dataclass decorator auto-generates __init__, __repr__, and __eq__ from class-level field annotations.
  • Default values are allowed; mutable defaults must use field(default_factory=...) to avoid shared state.
  • __post_init__ runs after the generated __init__ for validation or computed fields.
  • frozen=True makes instances immutable and hashable.
  • order=True generates comparison methods so instances can be sorted.
  • Dataclasses are ideal for simple data-holding classes, reducing boilerplate dramatically.

Interview Questions on Dataclasses

  • What is a dataclass and what boilerplate does it remove?
  • Which dunder methods does @dataclass generate automatically?
  • Why must mutable defaults use field(default_factory=...)?
  • What is __post_init__ used for?
  • What does frozen=True do?
  • How do dataclasses compare to named tuples and regular classes?
  • What does the order=True parameter add?

FAQ

Why can’t I use a list as a default field value? +

A bare default like items: list = [] would be shared by every instance, just like a mutable default argument. Dataclasses raise an error and require field(default_factory=list), which creates a fresh list per instance.

What is the difference between a dataclass and a named tuple? +

Named tuples are immutable and tuple-based, good for lightweight records. Dataclasses are mutable by default (unless frozen), support methods and defaults more flexibly, and are usually clearer for richer data objects.

When does __post_init__ run? +

Immediately after the auto-generated __init__ assigns the fields. Use it for validation or to compute derived attributes from the provided fields.

Does frozen=True make the object fully immutable? +

It blocks reassigning fields, raising FrozenInstanceError, and makes the instance hashable. However, a mutable field value (like a list inside it) can still be modified in place.