Course topics

By WebNest Studio

Python Tutorial

Dataclasses in Python

Many classes exist mainly to hold data: a Product with a name and a price, an Order with items and a status. Writing their __init__, __repr__ and __eq__ by hand is repetitive and error-prone. The dataclasses module generates them from type-annotated fields.

This lesson covers @dataclass, default values and field(), immutability with frozen=True, ordering, __post_init__ validation, slots, keyword-only fields, and converting dataclasses to dicts.

Basic Dataclasses

Decorate a class with @dataclass and declare fields with type annotations. Python generates __init__, a readable __repr__ and field-by-field __eq__. Type hints are documentation for tools; they are not enforced at runtime.

Defaults and field()

Fields with defaults must come after fields without. Mutable defaults are forbidden directly — use field(default_factory=list). field() also controls whether a field appears in __repr__ (repr=False for secrets), in comparisons, or in __init__ (init=False for computed fields).

Options: frozen, order, slots, kw_only

frozen=True makes instances immutable and hashable — good for value objects and dict keys. order=True generates comparison methods using fields in order. slots=True uses __slots__ for lower memory use and faster attribute access. kw_only=True forces keyword arguments for clearer construction.

Validation and Conversion

__post_init__ runs after the generated __init__ — use it to validate or derive values. dataclasses.asdict() and astuple() convert to plain structures (for JSON), and replace() creates a modified copy. For validation and parsing of external data (API input), Pydantic models (used by FastAPI) go further.

Examples

A basic dataclass versus a hand-written class

Python
from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float
    quantity: int = 0

    def total(self) -> float:
        return self.price * self.quantity

p = Product("Pen", 10.0, 5)
print(p)
print(p == Product("Pen", 10.0, 5), p.total())
print(Product("Book", 250.0))
Output
Product(name='Pen', price=10.0, quantity=5)
True 50.0
Product(name='Book', price=250.0, quantity=0)

field(), default_factory, __post_init__ and hidden fields

Python
from dataclasses import dataclass, field

@dataclass
class Order:
    order_id: str
    items: list[str] = field(default_factory=list)
    prices: list[float] = field(default_factory=list)
    api_token: str = field(default="", repr=False)
    total: float = field(init=False)

    def __post_init__(self):
        if not self.order_id.startswith("WN-"):
            raise ValueError("order id must start with WN-")
        self.total = sum(self.prices)

o = Order("WN-101", ["pen", "book"], [10, 250], api_token="secret")
print(o)
print(Order("WN-102").items is not Order("WN-103").items)
try:
    Order("101")
except ValueError as e:
    print("ValueError:", e)
Output
Order(order_id='WN-101', items=['pen', 'book'], prices=[10, 250], total=260)
True
ValueError: order id must start with WN-

frozen, order, slots, kw_only, asdict and replace

Python
from dataclasses import dataclass, asdict, astuple, replace, FrozenInstanceError

@dataclass(frozen=True, order=True, slots=True)
class Version:
    major: int
    minor: int
    patch: int = 0

@dataclass(kw_only=True)
class Settings:
    theme: str = "light"
    page_size: int = 20

v1, v2 = Version(3, 12), Version(3, 9, 4)
print(sorted([v1, v2]), v1 > v2, {v1: "current"})
try:
    v1.major = 4
except FrozenInstanceError as e:
    print("FrozenInstanceError:", e)
print(replace(v1, patch=1), asdict(v1), astuple(v2))
print(Settings(page_size=50))
try:
    Settings("dark")
except TypeError as e:
    print("TypeError:", e)
Output
[Version(major=3, minor=9, patch=4), Version(major=3, minor=12, patch=0)] True {Version(major=3, minor=12, patch=0): 'current'}
FrozenInstanceError: cannot assign to field 'major'
Version(major=3, minor=12, patch=1) {'major': 3, 'minor': 12, 'patch': 0} (3, 9, 4)
Settings(theme='light', page_size=50)
TypeError: Settings.__init__() takes 1 positional argument but 2 were given

Common Mistakes

  • Using items: list = [] as a default (ValueError: mutable default) — use field(default_factory=list).
  • Putting a field without a default after one with a default.
  • Assuming type hints are enforced at runtime; validate in __post_init__ or use Pydantic.
  • Mutating a frozen dataclass instead of using replace().

Key Points to Remember

  • @dataclass generates __init__, __repr__ and __eq__ from annotated fields.
  • Use field(default_factory=...) for mutable defaults; field() also controls repr/init/compare.
  • frozen, order, slots and kw_only options tailor behaviour.
  • __post_init__ validates or derives values; asdict/astuple/replace convert and copy.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.