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