Validate dataclass fields with __post_init__ in Python

Add custom validation to a Python dataclass inside __post_init__, raising ValueError or TypeError for invalid field values.

Easy Python 3.9+ Aug 9, 2026 OOP & classes 11 views 0 copies

Python code

43 lines
Python 3.9+
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class Product:
    name: str
    price: float
    quantity: int = 1
    category: Optional[str] = None

    def __post_init__(self):
        if not self.name or not isinstance(self.name, str):
            raise ValueError("name must be a non-empty string")
        if self.price < 0:
            raise ValueError("price must be non-negative")
        if not isinstance(self.price, (int, float)):
            raise TypeError("price must be numeric")
        if self.quantity <= 0:
            raise ValueError("quantity must be positive")
        if not isinstance(self.quantity, int):
            raise TypeError("quantity must be integer")
        if self.category is not None and not isinstance(self.category, str):
            raise TypeError("category must be a string or None")


if __name__ == "__main__":
    # Valid product
    product = Product(name="Laptop", price=999.99, quantity=2, category="electronics")
    print(product)

    # Invalid attempts
    for invalid in [
        dict(name="", price=10.5),
        dict(name="Phone", price=-5),
        dict(name="Phone", price=5, quantity=0),
        dict(name=123, price=5),
        dict(name="Phone", price=10, category=99),
    ]:
        try:
            Product(**invalid)
        except (ValueError, TypeError) as e:
            print(f"Invalid: {invalid} -> {type(e).__name__}: {e}")

Output

stdout
Product(name='Laptop', price=999.99, quantity=2, category='electronics')
Invalid: {'name': '', 'price': 10.5} -> ValueError: name must be a non-empty string
Invalid: {'name': 'Phone', 'price': -5} -> ValueError: price must be non-negative
Invalid: {'name': 'Phone', 'price': 5, 'quantity': 0} -> ValueError: quantity must be positive
Invalid: {'name': 123, 'price': 5} -> ValueError: name must be a non-empty string
Invalid: {'name': 'Phone', 'price': 10, 'category': 99} -> TypeError: category must be a string or None

How it works

__post_init__ runs automatically after the generated __init__ method, making it the ideal place to validate field values without overriding __init__. The checks enforce type and value constraints, raising ValueError for bad values and TypeError for wrong types. Because __post_init__ is called for every instantiation, invalid objects are rejected at creation time. This keeps the dataclass clean and declarative while centralizing validation logic in one place.

Common mistakes

  • Forgetting to check `None` before validating an optional field's type.
  • Using `if not self.name` when a field can legitimately be `0` or `False`.
  • Ordering type checks after value checks so a type error surfaces as a misleading `ValueError`.
  • Raising `Exception` instead of more specific `ValueError` or `TypeError`.

Variations

  1. Use `field(metadata={'validate': ...})` and loop over fields in `__post_init__` for DRY validation.
  2. Use Pydantic's `BaseModel` for automatic validation with custom validators.

Real-world use cases

  • Sanitizing user input when creating domain objects in a FastAPI endpoint before writing to a database.
  • Enforcing business rules like non-negative prices and positive stock counts in an e-commerce inventory system.
  • Validating configuration objects loaded from YAML or JSON so misconfigurations fail fast at startup.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from OOP & classes

Related tutorials and quizzes for this topic.