Validation and derived fields with __post_init__

dataclasses · Practical · 10 min

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.quantity

It 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 via object.__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.

Check yourself

  1. When does __post_init__ run?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/dataclasses.html#post-init-processing