Advanced Dataclasses: __post_init__, frozen, field(), slots, and Inheritance
You already know the basics of dataclasses — how to define fields, get automatic __init__ and __repr__, and set default values. Now it's time to unlock the advanced features.
Think of basic dataclasses like a pre-furnished apartment. It works great out of the box. Advanced dataclasses are like hiring an interior designer — you get custom validation, locked-down rooms, and exactly the layout you want.
In this tutorial, you'll learn five powerful techniques: validating data after creation, controlling default values, making objects immutable, adding memory-efficient slots, and building dataclass hierarchies with inheritance.
How Does __post_init__ Help You Validate Data?
The __post_init__ method runs automatically right after __init__ finishes. It's the perfect place to validate data, compute derived fields, or transform inputs.
You can also use __post_init__ to compute values that depend on other fields. This is great for derived data you don't want to pass in manually.
Notice the field(init=False) on area. This tells Python: "Don't include area in the constructor — I'll calculate it myself in __post_init__."
What Does field() Do and Why Do You Need It?
The field() function gives you fine-grained control over each field. The most common use is setting mutable default values safely.
The default_factory parameter takes a callable (a function with no arguments) that creates a fresh default value for each new instance. You can use list, dict, set, or any function you write.
The repr=False option hides a field from the printed representation. This is useful for long strings, passwords, or internal data you don't want cluttering the output.
How Do You Make a Dataclass Immutable with frozen=True?
By default, dataclass fields can be changed after creation. Adding frozen=True locks everything down. Any attempt to change a field raises a FrozenInstanceError.
Frozen dataclasses are automatically hashable, which means you can use them as dictionary keys or add them to sets. This is perfect for value objects that represent fixed data.
What Does slots=True Do for Performance?
In Python 3.10+, you can add slots=True to your dataclass decorator. This automatically generates __slots__ for your class, reducing memory usage and speeding up attribute access.
This is the cleanest way to use __slots__ in modern Python. You don't have to manually write the __slots__ tuple — the dataclass decorator does it for you based on your field definitions.
How Does Dataclass Inheritance Work?
Dataclasses support inheritance just like regular classes. A child dataclass gets all the fields from its parent, plus any new fields it defines.
The child's __init__ includes all parent fields first, then child fields. So Pet.__init__ takes name, sound, owner, and vaccinated in that order.
You can also use __post_init__ in child classes. If the parent has __post_init__, call super().__post_init__() to make sure both run.
Practice Exercises
Create a dataclass called Age with one field: years (int). Add a __post_init__ method that raises a ValueError with the message 'Age must be positive' if years is less than 0. Create an Age(25) and print it. Then use try/except to catch the error from Age(-5) and print 'Invalid age'.
This code tries to give each Playlist its own list of songs, but it will crash because mutable defaults are not allowed. Fix it using field(default_factory=list).
Read the code carefully. What will be printed? Think about what happens when you try to modify a frozen dataclass and whether two identical frozen instances are equal.
Create a dataclass BMI with fields weight_kg (float) and height_m (float). Add a computed field bmi (float, not in __init__) that is calculated in __post_init__ as weight_kg / (height_m ** 2). Round the bmi to 1 decimal place using round(). Create a BMI(70, 1.75) and print the bmi value.
Create a parent dataclass Employee with fields name (str) and department (str, default 'General'). Create a child dataclass Manager that inherits from Employee and adds a field team_size (int, default 5). Create a Manager('Alice') and print it.
Refactor this basic Product dataclass to add: (1) a __post_init__ that raises ValueError with 'Price must be positive' if price <= 0, (2) a computed field discounted_price (not in __init__) calculated as round(price * (1 - discount), 2). Create Product('Widget', 19.99, 0.15) and print its discounted_price.