Python Dataclasses: Using __post_init__ for Validation and Computed Fields
Use @dataclass with __post_init__ to validate inputs and compute derived fields after initialization. Learn how to enforce invariants, handle mutable defaults, work with frozen dataclasses, and avoid common pitfalls like shared mutable state and inheritance issues.
22 Aug 2025, 00:07 UTC

The short answer: validate in __post_init__, not __init__
When you need a class that carries data with minimal boilerplate, use @dataclass and put validation and derived-field logic in __post_init__. The decorator generates __init__, __repr__, and __eq__ for you; __post_init__ runs right after the generated __init__ finishes, so it's the natural place to check invariants (e.g., "email must contain @") or compute fields from other fields (e.g., extract a domain from an email). This keeps your class definition clean while still enforcing correctness at construction time.
How __post_init__ works
Python's dataclasses module (stdlib since 3.7) lets you declare fields with type hints, and the decorator builds the boilerplate. If you define __post_init__, the generated __init__ calls it after assigning all fields. Inside __post_init__, you can raise exceptions on invalid input or set additional attributes based on the validated fields. For example, a User class with name and email can check that the email looks plausible and then compute a domain field:
from dataclasses import dataclass
@dataclass
class User:
name: str
email: str
def __post_init__(self):
if '@' not in self.email:
raise ValueError(f"invalid email: {self.email}")
self.domain = self.email.split('@')[1]
u = User(name="Ada", email="[contact removed]")
print(u.domain) # example.com
Running this with Python 3.10+ shows validation and computed field in action. If you try User(name="Bad", email="not-an-email"), you get ValueError: invalid email: not-an-email.
Immutable dataclasses and computed fields
Setting frozen=True makes instances immutable, but then you cannot assign attributes normally, even in __post_init__. To set computed fields on a frozen dataclass, bypass the immutability with object.__setattr__:
@dataclass(frozen=True)
class Point:
x: float
y: float
def __post_init__(self):
object.__setattr__(self, 'magnitude', (self.x**2 + self.y**2) ** 0.5)
This is the supported pattern for computed fields on frozen dataclasses. Without object.__setattr__, you get dataclasses.FrozenInstanceError.
Mutable defaults and field ordering
Never use a mutable default like list or dict directly; all instances will share the same object. Instead, use field(default_factory=list) to create a fresh mutable default per instance:
from dataclasses import dataclass, field
@dataclass
class Team:
members: list = field(default_factory=list)
Fields without defaults must come before fields with defaults. If you need to reorder, use field(default=...) to give a field a default while keeping it in the right position.
Inheritance and slots
When you inherit from a dataclass and override __post_init__, you must call super().__post_init__() explicitly; otherwise the parent's validation or computed fields won't run. With slots=True (3.10+), you reduce memory but also prevent adding attributes outside __init__ and __post_init__. Slots do not interfere with __post_init__ itself.
Common mistakes and limits
Static type checkers (mypy, pyright) only check type hints; they do not enforce your __post_init__ logic. Runtime validation is your responsibility. If you raise an exception in __post_init__, the instance is partially constructed and will be garbage collected. Equality (__eq__) compares all fields by default, so large or recursive structures can be slow or hit RecursionError. Use repr=False on fields you don't want in equality or representation.
Verify the result
Run the example code and confirm that invalid email raises ValueError and that the domain field is computed. For frozen dataclasses, check that object.__setattr__ works in __post_init__. Use dataclasses.fields(User) to inspect field metadata programmatically. Profile memory with sys.getsizeof to compare slotted vs non-slotted dataclasses. If you need more control (e.g., type coercion, complex validation), consider pydantic; otherwise, dataclasses plus __post_init__ is enough for structured data modeling.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.