Lesson 29 · Intermediate · 19 min

Python Dataclasses

Declare data-focused classes without repetitive boilerplate.

dataclassesfieldsdefaultsasdict

An order is a small record: id, items, total, paid. Writing __init__, __repr__, and __eq__ by hand is a page of boilerplate. What does @dataclass generate for free?

A dataclass is a regular class with generated methods for initialization, display, and equality. It is a good fit for small records such as points, users, and configuration values. Fields are declared with annotations, and defaults are written like normal assignments. You will also use default_factory, validate in __post_init__, and update frozen records with replace.

Declare fields with annotations

The @dataclass decorator reads the annotated class attributes and writes an __init__ for you. Creating Point(2, 3) assigns x and y in order. The generated __repr__ makes the object readable in the console.

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

point = Point(2, 3)
print(point)

A small record

Add a default value

A field with a default can be omitted at construction. Fields without defaults must come first in the class body. The repr shows both the field names and their current values.

from dataclasses import dataclass

@dataclass
class User:
    name: str
    active: bool = True

print(User("Ada"))

Defaults in the field list

Compare and convert

Dataclasses generate __eq__ from their fields, so two objects with the same data compare equal. asdict returns a plain dictionary, which is convenient for JSON or test assertions. frozen=True prevents later attribute assignment.

from dataclasses import dataclass, asdict

@dataclass(frozen=True)
class Item:
    name: str
    price: float

item = Item("tea", 3.5)
print(asdict(item))
print(Item("tea", 3.5) == item)

Equality and a plain mapping

Mutable defaults need a factory

A list default in a dataclass would be shared by every instance, so Python rejects it. field(default_factory=list) creates a new list per object. The same rule applies to dictionaries, sets, and other mutable values.

from dataclasses import dataclass, field

@dataclass
class Order:
    drink: str
    tags: list[str] = field(default_factory=list)

first = Order("latte")
second = Order("tea")
first.tags.append("gift")
print(first.tags)
print(second.tags)
print(first.tags is second.tags)

One fresh list per instance

__post_init__ validates and derives

__post_init__ runs after the generated __init__. Use it to reject impossible values and compute fields that must stay consistent. It should validate the domain, not try to enforce type hints that Python never checked.

from dataclasses import dataclass

@dataclass
class Order:
    price: float
    quantity: int
    total: float = 0.0

    def __post_init__(self):
        if self.price < 0 or self.quantity <= 0:
            raise ValueError("invalid order")
        self.total = round(self.price * self.quantity, 2)

for values in [(4.5, 2), (-1, 1)]:
    try:
        print(Order(*values))
    except ValueError as error:
        print(type(error).__name__, error)

Validate, then derive the total

Update a frozen record with replace

frozen=True blocks attribute assignment, which makes a record safe to share. replace() returns a new instance with selected fields changed; the original stays intact. That pattern is useful for undo, history, and configuration snapshots.

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class Order:
    drink: str
    price: float

original = Order("latte", 4.5)
updated = replace(original, price=5.0)
print(original)
print(updated)
print(original == updated)

Change by copying, not mutating

Key points

  • @dataclass generates __init__, __repr__, and __eq__.
  • Fields are annotated class attributes.
  • Fields with defaults must come after fields without defaults.
  • asdict converts a dataclass instance to a dictionary.
  • frozen=True makes an instance immutable.
  • Use default_factory for mutable fields; validate in __post_init__; replace frozen records.

Common mistakes

  • Putting a field without a default after a field with one.
  • Expecting dataclass fields to validate types at runtime.
  • Trying to assign to a frozen instance.
  • Writing tags: list[str] = [] and creating one shared list for every instance.
Your turn

Create a frozen Item dataclass with name and price, then print asdict(Item('tea', 3.5)).

Try it yourself

Change the values, predict the output, then run the code. The Python runtime is downloaded the first time you press Run, so the first run can take a few seconds.

In-browser PythonRuns locally · no account
First run downloads Pyodide from a CDN. Use Run code after editing.

Practice this lesson

12 questions with immediate explanations.

See all 12 answersStart practice
← context managersitertools →