__post_init__ Processing
Overview
The `__post_init__` method is a special hook in dataclasses that runs immediately after the auto-generated `__init__` finishes. It allows you to execute custom logic before the caller receives the new instance. Mental model: `__init__` puts the raw data into the box, and `__post_init__` inspects or modifies the box before handing it back. Common use cases for this hook include: - Validating data to ensure it meets constraints, raising `ValueError` if it fails - Normalizing inputs by formatting or casting them to standardized forms - Computing dependent fields whose values rely on other provided arguments To compute a field dynamically without requiring the caller to provide it, you must declare it with `field(init=False)`. @dataclass class User: username: str is_admin: bool = field(init=False) def __post_init__(self): self.is_admin = self.username.startswith("admin_") When working with frozen dataclasses, normal attribute assignment inside `__post_init__` will raise a `FrozenInstanceError`. To bypass the freeze during initialization, you must use `object.__setattr__`. @dataclass(frozen=True) class Config: path: str def __post_init__(self): normalized = self.path.lower() object.__setattr__(self, "path", normalized) This rule and its implications are covered fully in frozen-dataclasses.
Because `@dataclass` generates `__init__` for you, you can't easily add validation or compute fields inside the constructor. `__post_init__` provides a hook to perform these actions automatically every time an instance is created.
Where used: Configuration parsing, Data validation pipelines, Pydantic-like early validation
Why learn this
- You can validate data at creation time rather than dealing with invalid state later.
- You can auto-calculate fields based on the raw inputs provided by the user.
Code walkthrough
from dataclasses import dataclass, field
@dataclass
class User:
username: str
is_admin: bool = field(init=False)
def __post_init__(self):
self.is_admin = self.username.startswith('admin_')
u = User('admin_alice')
print(u.is_admin)
Focus: `self.is_admin = ...` calculates the dependent field after `username` is set.
Aha moment
from dataclasses import dataclass
@dataclass
class Rectangle:
width: int
height: int
def __post_init__(self):
print(f'Validating {self.width}x{self.height}')
if self.width <= 0:
raise ValueError('Width must be positive')
Rectangle(10, 5)
Prediction: Will the `'Validating...'` print statement execute if you create a valid `Rectangle`?
Common guess: No, or I have to call `validate()` manually.
Yes, it prints `'Validating 10x5'`. `__post_init__` is guaranteed to run automatically at the very end of instantiation, intercepting and verifying the data before the caller receives the object.
Common mistakes
- Forgetting to type-hint the derived field: If you want a field to be computed in `__post_init__`, it still needs to be declared as a dataclass field. To avoid requiring it in `__init__`, you must use `field(init=False)`.
- Assigning to fields in a frozen dataclass: In a `frozen=True` dataclass, you cannot assign `self.attr = value` in `__post_init__`. You must use `object.__setattr__(self, 'attr', value)` instead.
Glossary
- normalizing
- Adjusting or changing data so it follows a standard, consistent format, like converting all text to lowercase. Example: `email.lower()`.
- hook
- A specific place or method provided by a system that allows you to insert your own custom code to run automatically. Example: `__post_init__`.
Recall questions
- When does the `__post_init__` method execute?
- What are two common use cases for `__post_init__`?
- If you want a field to be populated in `__post_init__` but not required during initialization, how do you define it?
Understanding checks
What is printed when this code is executed?
0.0
The auto-generated `__init__` sets `self.price = -50`. Then `__post_init__` runs, sees the negative price, and overwrites it with `0.0`.
Why does this code fail to run as intended?
`TypeError: __init__() missing 1 required positional argument: 'area'`
Because `area` is defined as a normal field, the auto-generated `__init__` requires it to be passed. To compute it in `__post_init__` without requiring it, it should be defined with `field(init=False)`.
What happens when you try to instantiate `Point(-5, 10)`?
It raises a `FrozenInstanceError`.
Because `Point` is a frozen dataclass, normal attribute assignment (like `self.x = 0`) fails, even inside `__post_init__`. You must use `object.__setattr__(self, 'x', 0)` to modify fields during initialization.
Practice tasks
Normalize the Username
Given this dataclass, modify it by adding a `__post_init__` method that normalizes `username` to be entirely lowercase.
Challenge
Data Validation on Init
Create a dataclass `Temperature` that takes one float field `celsius`. Implement `__post_init__` to raise a `ValueError` if `celsius` is below absolute zero (-273.15).