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 = [].** Usefield(default_factory=list). - Forgetting the type hint.
name = "x"without: stris 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
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.