Python Dataclasses: @dataclass, field(), frozen and __post_init__
Learn Python dataclasses: @dataclass, default values, field(default_factory), __post_init__ validation, frozen and order options, asdict and pandas.
Think about the OPD registration slip at a hospital in Indore. It is pre-printed with labelled boxes: patient name, age, phone number, department. Some boxes come pre-filled with sensible defaults, such as "Department: General Medicine" and "Visit type: New".
The receptionist does not design a new form for every patient. She fills in the boxes. Two slips with the same details describe the same visit. And anyone can read a slip at a glance, because every field is labelled.
A dataclass is that printed slip. The labelled boxes are fields. The pre-filled defaults are default values. Python prints the form for you: it writes the code that fills in the boxes (__init__), shows the slip clearly (__repr__) and compares two slips field by field (__eq__).
A Python dataclass is a class decorated with @dataclass, which reads the class's annotated fields and automatically generates methods such as __init__, __repr__ and __eq__, making it the standard way to define classes that mainly hold data.
This lesson follows Python operator overloading, where you wrote those methods by hand. Dataclasses arrived in Python 3.7; the slots and kw_only options need 3.10+. Examples target Python 3.12+.
Why use dataclasses in Python?
Compare a plain class for an order record with the dataclass version. Both behave the same way. One takes a dozen lines of repetitive code, the other takes four.
from dataclasses import dataclass
class OrderPlain:
def __init__(self, order_id, city, amount):
self.order_id, self.city, self.amount = order_id, city, amount
def __repr__(self):
return f"OrderPlain(order_id={self.order_id!r}, city={self.city!r}, amount={self.amount!r})"
def __eq__(self, other):
if not isinstance(other, OrderPlain):
return NotImplemented
return (self.order_id, self.city, self.amount) == (other.order_id, other.city, other.amount)
@dataclass
class Order:
order_id: int
city: str
amount: float
print(OrderPlain(1041, "Indore", 1450.0))
print(Order(1041, "Indore", 1450.0))
print(Order(1041, "Indore", 1450.0) == Order(1041, "Indore", 1450.0))The hand-written version has a classic bug risk: add a field later and you must remember to update three methods. With @dataclass, adding a field is one line. The type hints from Python type hints double as the field list.
You write the fields; the decorator writes the methods.
Type hints in a dataclass are not enforced at runtime. Order("x", 5, None) is accepted without complaint. Use __post_init__, shown below, when you need checks.
Default values and field(default_factory)
Fields can have defaults, written like normal assignments. Fields without defaults must come first, just like function parameters with and without default values.
A list or dict default needs special care. In a plain function, a default list is created once and shared by every call. Dataclasses refuse that mistake outright. Use field(default_factory=list), which calls list() afresh for every new object.
from dataclasses import dataclass, field
@dataclass
class Customer:
name: str
city: str = "Indore"
tier: str = "Silver"
tags: list[str] = field(default_factory=list)
a = Customer("Asha")
b = Customer("Ravi", city="Ujjain", tier="Gold")
a.tags.append("festive-buyer")
print(a)
print(b)
try:
@dataclass
class Broken:
tags: list = []
except ValueError:
print("ValueError: a plain [] default is not allowed, use default_factory")default_factory runs once per object, so each object gets its own list.
Quick check
Which is the correct way to give a dataclass field an empty list as its default?
default_factory calls list() for each new object. A plain [] default is rejected with ValueError.
Validation and derived fields with __post_init__
The generated __init__ only stores values. If you define a method named __post_init__, the dataclass calls it right after storing the fields. That is the place for validation and for fields calculated from other fields. Mark calculated fields with field(init=False) so they are not constructor arguments.
from dataclasses import dataclass, field
@dataclass
class InvoiceLine:
item: str
qty: int
unit_price: float
gst_rate: float = 0.18
total: float = field(init=False)
def __post_init__(self):
if self.qty <= 0:
raise ValueError(f"{self.item}: qty must be positive, got {self.qty}")
if self.gst_rate not in (0, 0.05, 0.12, 0.18, 0.28):
raise ValueError(f"{self.item}: unusual GST rate {self.gst_rate}")
self.total = round(self.qty * self.unit_price * (1 + self.gst_rate), 2)
print(InvoiceLine("A4 paper ream", 4, 245.0))
print(InvoiceLine("Notebook", 10, 60.0, gst_rate=0.12).total)
for args in [("Stapler", 0, 120.0), ("Pen box", 2, 90.0, 0.4)]:
try:
InvoiceLine(*args)
except ValueError as err:
print("Rejected:", err)Raising errors in __post_init__ means no invalid record can ever exist. For larger projects, a dedicated error class from Python custom exceptions makes these failures easy to catch.
Frozen and ordered dataclasses
The decorator takes options. frozen=True makes objects read-only after creation: any assignment raises FrozenInstanceError. Frozen dataclasses are also hashable, so they work as dict keys and set members. order=True adds <, > and friends, comparing fields in the order they are declared.
from dataclasses import dataclass, FrozenInstanceError
@dataclass(frozen=True, order=True)
class StoreMonth:
year: int
month: int
store: str
keys = [StoreMonth(2026, 11, "Palasia"), StoreMonth(2026, 9, "Rajwada"),
StoreMonth(2025, 12, "Palasia")]
print(sorted(keys)[0])
sales = {StoreMonth(2026, 11, "Palasia"): 48200}
print(sales[StoreMonth(2026, 11, "Palasia")])
try:
keys[0].month = 12
except FrozenInstanceError:
print("FrozenInstanceError: StoreMonth is read-only")Sorting compares year first, then month, then store, exactly like comparing tuples. To change a frozen object, create a modified copy with dataclasses.replace(obj, month=12).
Quick check
What does frozen=True do to a dataclass?
Assigning to a field of a frozen instance raises FrozenInstanceError, and frozen dataclasses get a __hash__.
From dataclasses to dicts, JSON and pandas
Analysis code usually ends in a table. asdict() converts a dataclass, including nested ones, into a plain dict. A list of those dicts goes straight into a pandas DataFrame or into json.dumps().
import json
from dataclasses import dataclass, asdict, replace
import pandas as pd
@dataclass
class Order:
order_id: int
city: str
amount: float
orders = [Order(1041, "Indore", 1450.0), Order(1042, "Bhopal", 980.0),
Order(1043, "Indore", 2300.0)]
print(json.dumps(asdict(orders[0])))
df = pd.DataFrame([asdict(o) for o in orders])
print(df.groupby("city")["amount"].sum().to_dict())
fixed = replace(orders[1], amount=1080.0) # copy with one field changed
print(fixed, orders[1].amount)The DataFrame side of this is covered in the pandas DataFrame tutorial.
slots and kw_only
@dataclass(slots=True) stores fields in fixed slots instead of a per-object dict, which saves memory when you create hundreds of thousands of records. @dataclass(kw_only=True) forces every field to be passed by name, as in Order(order_id=1041, city="Indore", amount=1450.0), which prevents mixing up arguments of the same type. Both need Python 3.10 or newer.
Dataclass vs namedtuple vs TypedDict vs dict
| Option | Mutable? | Methods? | Typical use |
|---|---|---|---|
dict |
Yes | No | Quick, one-off records and JSON data |
TypedDict |
Yes (it is a dict) | No | Typing JSON-like dicts for mypy |
namedtuple |
No | Few | Lightweight read-only rows, tuple unpacking |
@dataclass |
Yes, or no with frozen=True |
Yes | Records with defaults, validation and behaviour |
The dataclasses documentation lists every option.
Common mistakes with Python dataclasses
- Expecting type checks at runtime. Hints are not enforced. Validate in
__post_init__. - Default fields before required ones.
city: str = "Indore"followed byamount: floatraisesTypeError. Put required fields first. - Using
[]or{}as a default. Usefield(default_factory=list). - Mutating a frozen dataclass. Use
dataclasses.replace()to make a changed copy. - Forgetting
order=True. Without it,sorted()on dataclass objects raisesTypeError. - Writing
__init__yourself in a dataclass. It replaces the generated one. Use__post_init__instead.
Key takeaways
@dataclasswrites__init__,__repr__and__eq__from annotated fields.- Required fields come before fields with defaults.
- Use
field(default_factory=list)for mutable defaults. __post_init__validates input and computes derived fields.frozen=Truegives read-only, hashable records;order=Trueenables sorting.asdict()turns records into dicts for pandas and JSON.
With OOP done, the next section starts with Python iterators, the protocol behind every for loop. Modelling business records with dataclasses before loading them into pandas is practised in Debugshala's data analytics batches taught at Old Palasia.
FAQ
What is a dataclass in Python?
A dataclass is a regular class marked with the @dataclass decorator. You declare fields with type hints, and Python generates the __init__, __repr__ and __eq__ methods for you. It is the standard choice for classes that mainly store data, such as orders, customers or configuration settings.
What is the difference between a dataclass and a normal class?
There is no runtime difference in kind: a dataclass is a normal class with some methods written automatically. A normal class needs a hand-written __init__, __repr__ and __eq__. A dataclass generates them from the field list, which removes repetitive code and keeps the methods in sync when fields change.
When should I use __post_init__ in a dataclass?
__post_init__ in a dataclass?Use __post_init__ when you need to validate fields or compute a field from others after the generated __init__ has stored the values. Examples are rejecting a negative quantity or calculating a total including GST. Mark computed fields with field(init=False) so callers do not pass them.
Are Python dataclasses faster than dictionaries?
Attribute access on a dataclass is about as fast as a dict lookup, and with slots=True instances use noticeably less memory. The bigger benefit is correctness: named fields, defaults and validation catch typos that a dict would silently accept. For very large tables, a pandas DataFrame remains the better tool.
Where can I learn Python dataclasses with practice in Indore?
Debugshala's data analytics course at Old Palasia, Indore, covers Python OOP, including dataclasses, before moving on to pandas and SQL. It accepts non-IT graduates and offers a free demo class. Between sessions, solve dataclass and OOP practice questions on Debugshala Test.
From the team behind this site
Go further with the Data Analytics Course at Debugshala
These tutorials are written by Debugshala, a training and placement institute at Old Palasia, Indore. The classroom course adds live mentoring, real projects and interview preparation.
- Offline classes at Old Palasia, Indore
- Free demo class before you pay
- Non-IT, fresher and year-gap friendly
- Placement assistance
700+ students placed · 200+ hiring partners · 4.9★ Google rating