Q&A library Python

How do I validate the values in a dataclass?

Python Advanced dataclassespost-initvalidation

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.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project