Real models need invariants checked at construction and values computed from other fields.
You normally don't write __init__ for a dataclass (defining one replaces the generated one). Instead, define __post_init__(self): the generated __init__ calls it after assigning all fields.
Use it to:
- validate:
if self.quantity < 0: raise ValueError(...), - normalise:
self.email = self.email.strip().lower(), - derive fields: compute values from other fields.
A derived field shouldn't be a constructor parameter. Declare it with field(init=False), then set it in __post_init__:
total: float = field(init=False)
def __post_init__(self):
self.total = self.price * self.quantityIt still shows up in repr and comparisons unless you also pass repr=False / compare=False.
Keep in mind:
- Validation runs only at construction. Later assignments (
item.quantity = -5) are not checked; make the class frozen or use properties if that matters. - With
frozen=True, set derived fields viaobject.__setattr__(self, "total", ...). - For per-field conversion on assignment, a descriptor or a library such as Pydantic or attrs (with converters) may fit better.
Example
from dataclasses import dataclass, field
@dataclass
class LineItem:
sku: str
price: float
quantity: int = 1
total: float = field(init=False)
def __post_init__(self):
self.sku = self.sku.strip().upper()
if self.quantity < 1:
raise ValueError(f"quantity must be >= 1, got {self.quantity}")
self.total = round(self.price * self.quantity, 2)
item = LineItem(" ab-12 ", 9.99, 3)
print(item) # LineItem(sku='AB-12', price=9.99, quantity=3, total=29.97)
try:
LineItem("x", 1.0, 0)
except ValueError as e:
print(e) # quantity must be >= 1, got 0
item.quantity = 0 # not validated after construction!Remember
- __post_init__ runs after the generated __init__
- field(init=False) declares derived fields set in __post_init__
- Validation only covers construction, not later assignments
Watch out for
- Defining __init__ yourself, which silently replaces the generated one.