Free demoJob-oriented courses at Debugshala, Old Palasia, Indore. Freshers, non-IT and year-gap welcome.Book a free demo +91-8982385539

Python Dataclasses: @dataclass, field(), frozen and __post_init__

Lesson 43 of 52 · 8 min · Updated 13 Nov 2026

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.

Python
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.

Python dataclass: the @dataclass decorator reads annotated fields order_id, city and amount and generates __init__, __repr__ and __eq__ methods

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.

Python
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")
Python dataclass default_factory: a shared empty-list default would give every object the same list, while field(default_factory=list) creates a new list per object

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?

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.

Python
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.

Python
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?

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().

Python
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 by amount: float raises TypeError. Put required fields first.
  • Using [] or {} as a default. Use field(default_factory=list).
  • Mutating a frozen dataclass. Use dataclasses.replace() to make a changed copy.
  • Forgetting order=True. Without it, sorted() on dataclass objects raises TypeError.
  • Writing __init__ yourself in a dataclass. It replaces the generated one. Use __post_init__ instead.

Key takeaways

  • @dataclass writes __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=True gives read-only, hashable records; order=True enables 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.

Take the free Python Programming quiz10 quick questions with explanations. No sign-up needed, and you can come back to this lesson after.

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?

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

Python Operator Overloading: Magic Methods __add__ and __eq__Python Iterators: iter(), next() and the Iterator Protocol