Skip to Content
Python dataclasses: A Class Without Writing __init__ by Hand
Intermediate
0 likes
Tutorials

Python dataclasses: A Class Without Writing __init__ by Hand

If a class mostly holds data, @dataclass writes __init__, a readable printout and equality for you. Use field(default_factory=list) for lists, __post_init__ for checks and frozen=True for values that must not change.

If a class mostly holds data, and you're writing __init__ just to copy arguments into self, use a dataclass. It writes __init__, a readable printout and equality checks for you, from a list of fields. If your class is mostly behavior, with logic in every method, a dataclass helps less, and a normal class is fine.

Skip ahead: If you only want the syntax, read "The same class in four lines". Read "The list default trap" before you give any field a list.

The class you keep writing

class Product:
    def __init__(self, name, price, stock=0):
        self.name = name
        self.price = price
        self.stock = stock


coffee = Product("Coffee", 95.0)
print(coffee)
print(coffee == Product("Coffee", 95.0))
<__main__.Product object at 0x7fe6d469aa50>
False

Ten lines, and the result still isn't great. Printing shows a memory address, not the data. Two products with the same values are not equal, because by default Python compares whether they're the same object. You'd have to write __repr__ and __eq__ yourself to fix that.

The same class in four lines

from dataclasses import dataclass


@dataclass
class Product:
    name: str
    price: float
    stock: int = 0


coffee = Product("Coffee", 95.0)
print(coffee)
print(coffee == Product("Coffee", 95.0))
coffee.stock += 10
print(coffee.stock)
Product(name='Coffee', price=95.0, stock=0)
True
10

The @dataclass decorator reads the fields from the class body and generates __init__, __repr__ and __eq__. The type hints (str, float, int) are required, because that's how it finds the fields. But they're not checked when the program runs: Product("Coffee", "cheap") works without an error. If you want that checked, use a type checker like mypy, or validate in __post_init__ (below).

Fields with defaults must come after fields without them, just like function parameters.

The list default trap

from dataclasses import dataclass

try:
    @dataclass
    class Order:
        customer: str
        items: list = []
except ValueError as error:
    print("ValueError:", error)
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory

Dataclasses refuse this on purpose. A list default would be created once and shared by every Order, the same bug as a mutable default argument in a normal function. The fix is field(default_factory=list), which calls list() to make a new list for each object:

from dataclasses import dataclass, field


@dataclass
class Order:
    customer: str
    items: list[str] = field(default_factory=list)


first = Order("Ana")
second = Order("Ben")
first.items.append("Coffee")
print(first)
print(second)
Order(customer='Ana', items=['Coffee'])
Order(customer='Ben', items=[])

Same idea for dictionaries and sets: field(default_factory=dict).

Checking values with __post_init__

The generated __init__ calls __post_init__ at the end, if you define one. That's the place for validation and cleaning:

from dataclasses import dataclass


@dataclass
class Product:
    name: str
    price: float
    stock: int = 0

    def __post_init__(self):
        self.name = self.name.strip()
        if self.price < 0:
            raise ValueError(f"price can't be negative: {self.price}")


print(Product("  Coffee ", 95.0))
try:
    Product("Bread", -35.0)
except ValueError as error:
    print("ValueError:", error)
Product(name='Coffee', price=95.0, stock=0)
ValueError: price can't be negative: -35.0

Frozen: values that must not change

Add frozen=True and the object can't be changed after it's created. Use it for things like settings or a money amount, where a change somewhere far away would be a bug:

from dataclasses import dataclass, FrozenInstanceError, replace


@dataclass(frozen=True)
class Settings:
    currency: str = "PHP"
    vat_rate: float = 0.12


settings = Settings()
try:
    settings.vat_rate = 0.0
except FrozenInstanceError as error:
    print("FrozenInstanceError:", error)

test_settings = replace(settings, vat_rate=0.0)
print(settings)
print(test_settings)
FrozenInstanceError: cannot assign to field 'vat_rate'
Settings(currency='PHP', vat_rate=0.12)
Settings(currency='PHP', vat_rate=0.0)

replace() makes a copy with some values changed, and leaves the original alone. A frozen dataclass can also be a dictionary key or go into a set, which a normal dataclass can't.

Methods still work

A dataclass is a normal class. You can add methods and properties:

from dataclasses import dataclass


@dataclass
class Product:
    name: str
    price: float
    stock: int = 0

    def stock_value(self):
        return self.price * self.stock


print(Product("Coffee", 95.0, 12).stock_value())
1140.0

When not to use one

  • The class is mostly behavior. A class that wraps a database connection or runs a process gains little from generated __eq__ and __repr__.
  • You need strict type checking and conversion of input, for example from JSON or a web form. That's what libraries like Pydantic are for. A dataclass only stores what you give it.
  • A dictionary is enough. If the data lives for three lines and goes straight into json.dumps, a class adds nothing.

Common mistakes

  • **items: list = [].** Use field(default_factory=list).
  • Forgetting the type hint. name = "x" without : str is a plain class variable, not a field, and it won't be in __init__.
  • Expecting the types to be checked. They aren't, at run time.
  • A default field before a required one. TypeError: non-default argument ... follows default argument.

What happens if you skip it

Nothing breaks. You just write and maintain __init__, __repr__ and __eq__ by hand for every data class, and the day you add a field you have to remember to add it in three places. Forget one, and two products with different stock compare as equal.

If you add a category field to Product tomorrow, how many places in your current code would need to change?

About

Turning a plain data class into a dataclass, list defaults with default_factory, validation in __post_init__, frozen dataclasses and replace(). After this, you can write data classes in a few lines and know when not to.

Responsible
Christopher R
Last Update
09/14/2026
Reading Time
5 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.