🐍 Python Course · Stage 1 · Lesson 17 of 89 · Dataclasses
Course Overview →

🟩 Start with the Beginner tab. Read it fully before moving on.

← Object-Oriented Python Enums →
thecodex.expert · The Codex Family of Knowledge
Python

Dataclasses in Python

The @dataclass decorator auto-generates __init__, __repr__, and __eq__ from type-annotated fields — the modern, concise way to write classes that mainly hold data.

Python 3.13 docs.python.org Last verified:
Canonical Definition

A dataclass is a class decorated with @dataclass from the dataclasses module (PEP 557, Python 3.7+). The decorator reads the annotated fields of the class and generates boilerplate methods automatically, including __init__, __repr__, and __eq__.

🟩 Beginner

Classes without the boilerplate

What you’ll learn: How @dataclass auto-generates __init__, __repr__, and __eq__ from annotated fields, and how to give fields default values. The modern default for any class that mainly holds data.

How to read this tab: Rewrite one of your existing simple classes as a dataclass. Watch the boilerplate disappear — that is the lesson.

⏱ 30 min📄 2 sections🔶 Prerequisite: Object-Oriented Python
The idea

A dataclass writes the tedious parts of a class for you. You declare what fields the class has; Python generates the constructor, a readable printout, and equality comparison automatically. Less code, fewer bugs.

Annotations are how the decorator finds fields. x: int is a dataclass field. x = 5 without an annotation is a plain class attribute the decorator ignores. This is the single most important rule — no annotation, no field.

The problem dataclasses solve

Writing a simple data-holding class the traditional way involves a lot of repetition. The @dataclass decorator eliminates it.

Pythonwhy_dataclass.py
# WITHOUT dataclass — manual boilerplate
class PointOld:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"PointOld(x={self.x}, y={self.y})"
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

# WITH dataclass — same behaviour, far less code
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
print(p)                  # Point(x=1.0, y=2.0)  ← generated __repr__
print(p == Point(1.0, 2.0))  # True  ← generated __eq__
print(p.x, p.y)           # 1.0 2.0
Type annotations are required

A dataclass field must have a type annotation — that is how the decorator finds it. x: float is a field. x = 5 without an annotation is a plain class attribute and is ignored by the dataclass machinery. If you don't want to constrain the type, annotate with typing.Any.

💡

Same ordering rule as function parameters: fields without defaults must come before fields with defaults. name: str then age: int = 0 works; the reverse raises TypeError at class definition.

Fields and default values

Pythondefaults.py
from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int = 0              # default value
    active: bool = True       # default value
    role: str = "member"

u1 = User("Priya")            # uses defaults
print(u1)  # User(name='Priya', age=0, active=True, role='member')

u2 = User("Arjun", 30, role="admin")
print(u2)  # User(name='Arjun', age=30, active=True, role='admin')

# Fields WITHOUT defaults must come before fields WITH defaults
# (same rule as function parameters)
# @dataclass
# class Bad:
#     x: int = 0
#     y: int       # TypeError — non-default after default

✅ Beginner tab complete

  • I can write a @dataclass with annotated fields and no manual __init__
  • I know fields must have type annotations to be recognised
  • I can give fields default values (non-default fields must come first)
  • I know the generated __repr__ and __eq__ work automatically

Continue to Enums →

🔵 Intermediate

frozen, order, slots, field(), __post_init__

What you’ll learn: The decorator options (frozen for immutability, order for comparisons, slots for memory), field(default_factory=...) for mutable defaults, and __post_init__ for validation.

How to read this tab: The mutable-default rule is critical: never use list/dict directly as a default. Learn field(default_factory=list).

⏱ 30 min📄 2 sections🔶 Prerequisite: Beginner tab
💡

frozen=True gives you immutability AND hashability. A frozen dataclass can be a dict key or set member because Python generates __hash__ for it. Use it for value objects like coordinates, money, or configuration that should never change after creation.

Decorator options

The @dataclass decorator accepts arguments that control what it generates. The most useful are frozen (immutability), order (comparison operators), and slots (memory optimisation).

Pythonoptions.py
from dataclasses import dataclass

# frozen=True — immutable, and hashable (usable as dict key / in a set)
@dataclass(frozen=True)
class Coordinate:
    lat: float
    lon: float

c = Coordinate(19.07, 72.87)
# c.lat = 0   # raises FrozenInstanceError
locations = {c: "Mumbai"}   # works — frozen dataclasses are hashable

# order=True — generates __lt__, __le__, __gt__, __ge__
@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int

versions = [Version(1, 2, 0), Version(1, 0, 5), Version(2, 0, 0)]
print(sorted(versions))   # sorted by (major, minor, patch) tuple order
# [Version(1,0,5), Version(1,2,0), Version(2,0,0)]

# slots=True (Python 3.10+) — less memory, faster attribute access
@dataclass(slots=True)
class Particle:
    x: float
    y: float
    z: float
# No per-instance __dict__ — significant memory savings at scale

Never use a mutable default directly — Python raises ValueError to protect you. items: list = [] fails at class definition. Use items: list = field(default_factory=list) so each instance gets its own fresh list.

field() and __post_init__

Mutable defaults (lists, dicts) cannot be used directly — the same gotcha as mutable default arguments. The field() function with default_factory solves this. __post_init__ runs validation or derived-value computation after the generated __init__.

Pythonfield_postinit.py
from dataclasses import dataclass, field

@dataclass
class ShoppingCart:
    customer: str
    # WRONG: items: list = []  — shared mutable default, raises ValueError
    # RIGHT: use default_factory
    items: list = field(default_factory=list)
    tags: dict = field(default_factory=dict)

c1 = ShoppingCart("Priya")
c1.items.append("book")
c2 = ShoppingCart("Arjun")
print(c2.items)   # []  — independent, not shared

# field() options
@dataclass
class Account:
    name: str
    balance: float = 0.0
    # exclude from __repr__ and __init__
    _id: int = field(default=0, repr=False)
    # computed, not passed to __init__
    display: str = field(init=False, default="")

    def __post_init__(self):
        # runs after __init__ — validation and derived values
        if self.balance < 0:
            raise ValueError("Balance cannot be negative")
        self.display = f"{self.name} (${self.balance:.2f})"

a = Account("Priya", 100.0)
print(a.display)   # "Priya ($100.00)"
Commonly confused
Never use a mutable default directly. items: list = [] raises ValueError in a dataclass (Python protects you here, unlike plain functions). Always use field(default_factory=list).
dataclass vs namedtuple vs NamedTuple vs TypedDict. Use a dataclass for mutable records with methods. Use NamedTuple for immutable lightweight records. Use TypedDict for dict-shaped data with type hints (e.g. JSON). dataclass is the most flexible and the usual default.

✅ Intermediate tab complete

  • I use field(default_factory=list) for mutable defaults — never items: list = []
  • I can make an immutable, hashable dataclass with frozen=True
  • I can generate comparison operators with order=True
  • I can validate or compute derived values in __post_init__

Continue to Enums →

🔴 Expert

Code generation and the introspection API

What you’ll learn: How @dataclass generates methods via exec() at class-creation time (zero runtime overhead), plus fields(), asdict(), astuple(), InitVar, and __dataclass_fields__.

How to read this tab: Read when building libraries or when you need to introspect dataclass structure programmatically.

⏱ 20 min📄 1 section🔶 Prerequisite: After Stage 2

How @dataclass generates code at class-creation time

The @dataclass decorator runs once, when the class is defined. It inspects __annotations__, builds the source code for each method as a string, compiles it with exec(), and attaches the resulting functions to the class. This is why dataclasses have zero runtime overhead compared to hand-written classes — the generated methods are ordinary Python methods, indistinguishable from ones you wrote yourself.

Pythondataclass_internals.py
from dataclasses import dataclass, fields, asdict, astuple

@dataclass
class Point:
    x: int
    y: int

# Introspection API
p = Point(1, 2)
for f in fields(Point):
    print(f.name, f.type, f.default)   # x int <factory>, y int <factory>

# Conversion helpers
print(asdict(p))    # {'x': 1, 'y': 2}  — recursive
print(astuple(p))   # (1, 2)

# The generated __init__ is a real function you can inspect
import inspect
print(inspect.getsource(Point.__init__))
# def __init__(self, x: int, y: int) -> None:
#     self.x = x
#     self.y = y

# __dataclass_fields__ holds the field metadata
print(Point.__dataclass_fields__.keys())  # dict_keys(['x', 'y'])

The InitVar type marks a parameter that is passed to __init__ and forwarded to __post_init__ but not stored as a field. Combined with field(init=False) and __post_init__, this gives precise control over the initialisation pipeline — useful for fields whose values are derived from other fields rather than passed in directly.

✅ Expert tab complete

  • I know @dataclass generates real methods at definition time with zero runtime overhead
  • I can use fields() and asdict() to introspect and convert dataclasses
  • I know InitVar marks init-only parameters forwarded to __post_init__

Continue to Enums →

Sources

1
Python Standard Library — dataclasses. docs.python.org/3/library/dataclasses.html.
2
PEP 557 — Data Classes. peps.python.org/pep-0557/.
3
PEP 681 — Data Class Transforms. peps.python.org/pep-0681/.
4
Python Standard Library — dataclasses.field, asdict, fields. docs.python.org/3/library/dataclasses.html#dataclasses.field.
Source confidence: High Last verified: Primary source: Python dataclasses (PEP 557)