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.
Python code
43 linesfrom 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
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
- Use `field(metadata={'validate': ...})` and loop over fields in `__post_init__` for DRY validation.
- 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
More from OOP & classes
- Add property getter setter validation in Python easy
- Binary Tree Inorder Traversal in Python easy
- Borg pattern shared state in Python medium
- Bridge Pattern in Python: Separate Abstraction from Implementation medium
- Composable Predicates with the &, |, ~ Operators in Python medium
- Composition over Inheritance: How to Build a Wallet Account in Python easy
Keep learning
Related tutorials and quizzes for this topic.