How do I validate the values in a dataclass?
Short answer
Use __post_init__ to check the values after the generated constructor has run, raising if they are invalid. For heavier validation and type coercion, a library like Pydantic is a better fit.
The short version
A dataclass does not validate anything; the type annotations are hints, not checks. Add a __post_init__ method, which runs immediately after the generated __init__, and raise there if a value is invalid. For serious validation and type coercion, reach for Pydantic instead.
Validating with post_init
from dataclasses import dataclass
@dataclass
class Order:
quantity: int
price: float
def __post_init__(self):
if self.quantity < 0:
raise ValueError("quantity cannot be negative")
if self.price < 0:
raise ValueError("price cannot be negative")
Order(quantity=-1, price=5.0) # ValueError: quantity cannot be negative
__post_init__ is called automatically at the end of the constructor, so the object can never exist in an invalid state.
The annotations do not check anything
This surprises people. A dataclass will happily accept the wrong type.
Order(quantity="not a number", price="free") # no error at all
The annotations are documentation and tooling hints. Python does not enforce them at runtime, so if you want a guarantee, you must write the check yourself.
Normalising as well as checking
__post_init__ is also the right place to tidy values, since it runs before anyone can use the object.
def __post_init__(self):
self.city = self.city.strip().title()
This guarantees every instance is normalised, rather than relying on each caller to remember.
Derived fields
A field computed from the others can be declared and filled in __post_init__, marked so it is not a constructor argument.
from dataclasses import dataclass, field
@dataclass
class Order:
quantity: int
price: float
total: float = field(init=False)
def __post_init__(self):
self.total = self.quantity * self.price
init=False keeps total out of the constructor signature, because it is derived rather than supplied.
When to use Pydantic instead
If you need type coercion (turning the string "5" into the integer 5), detailed error messages listing every problem at once, or validation of nested structures, hand-written checks become tedious. Pydantic does all of this from the annotations you have already written, which is why it is the standard choice for parsing external data such as an API payload or a configuration file.
Dataclasses remain the right tool for internal objects whose values you already trust, where you want a light record with no dependency. Use __post_init__ for the occasional sanity check, and Pydantic when validation is the point of the object.
Fail early, not late
The value of validating in the constructor is that a bad value is rejected at the moment it enters your program, with a message naming the field. The alternative, letting it flow through and cause a strange error five steps later, is far harder to diagnose, which is exactly the cost that early validation avoids.