PYTHON / OBJECT-ORIENTED PYTHON
Dataclasses
Use @dataclass to generate __init__, __repr__, and __eq__ from annotated fields, and control them with field(), frozen, and order.
What you will learn
- Declare fields as annotated class-level names; order sets __init__ parameter order
- Use field(default_factory=list) for mutable defaults instead of a literal []
- Compute derived fields in __post_init__ and declare them with field(init=False)
- Pass frozen=True and order=True to get hashable, comparable value objects
Understanding Dataclasses
A dataclass is a normal class whose boilerplate is written for you. When the @dataclass decorator runs, it reads the class's __annotations__ in definition order, builds source code for __init__, __repr__, and __eq__ as strings, compiles it, and attaches the resulting functions to the class. That is why the annotation is not optional decoration here: a name with no annotation is just a plain class attribute and will not become a field, so Point with `x = 0` and no `: float` produces an __init__ that takes no x at all.
Field order is the constructor signature. Because the generated __init__ lists parameters in annotation order, a field with a default cannot be followed by one without a default, exactly as in a hand-written function, and Python raises TypeError while the class body is being processed. Defaults are stored on the class and shared, so a mutable literal like `tags: list = []` would be one list for every instance; dataclasses refuse that outright with a ValueError and point you at field(default_factory=list), which calls the factory once per instance inside __init__.
The generated __eq__ compares the tuple of all fields, but only after checking that other.__class__ is self.__class__, so a subclass instance never equals its parent instance and comparison with an unrelated type returns NotImplemented rather than False. Because defining __eq__ normally sets __hash__ to None, an ordinary dataclass is unhashable; frozen=True changes that by generating a __setattr__ that raises FrozenInstanceError and a __hash__ built from the fields. order=True adds __lt__ and friends that compare the same field tuple, which is why field order also decides sort order.
from dataclasses import dataclass, field
dataclass
class Point:
x: float
y: float
label: str = "origin"
tags: list[str] = field(default_factory=list)
def distance_to(self, other):
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
a = Point(0.0, 0.0)
b = Point(3.0, 4.0, "target")
print(a)
print(b)
print(a == Point(0.0, 0.0))
print(a.distance_to(b))
b.tags.append("visited")
print(b.tags, a.tags)
@dataclass turns annotated class-level fields into a specification from which Python generates __init__, __repr__, __eq__, and optionally ordering and hashing.
Worked examples
Frozen and ordered value objects
Shows how frozen=True and order=True make a dataclass hashable, sortable, and safe from attribute rebinding.
from dataclasses import dataclass, replace
dataclass(frozen=True, order=True)
class Version:
major: int
minor: int
v1 = Version(1, 4)
v2 = Version(1, 10)
print(v1 < v2)
print(sorted([v2, v1]))
status = {v1: "stable"}
print(status[Version(1, 4)])
print(replace(v1, minor=5))
try:
v1.minor = 9
except Exception as exc:
print(type(exc).__name__, exc)
Example explained
Line 1order=True generates __lt__ comparing (major, minor) as a tuple, so 1.4 sorts before 1.10.
Line 2frozen=True keeps __hash__ from being set to None, so Version(1, 4) finds the dict key made from an equal but distinct object.
Line 3replace() builds a new instance by calling __init__ with the old field values plus the overrides; it never mutates v1.
Line 4Assignment fails because frozen=True installs a __setattr__ that raises FrozenInstanceError instead of storing the value.
Derived fields and fields excluded from comparison
Uses __post_init__ for a computed field and field() flags to keep a note out of the repr and out of equality.
from dataclasses import dataclass, field
dataclass
class Order:
unit_price: float
quantity: int
note: str = field(default="", compare=False, repr=False)
total: float = field(init=False)
def __post_init__(self):
self.total = round(self.unit_price * self.quantity, 2)
o1 = Order(2.5, 4, note="rush")
o2 = Order(2.5, 4)
print(o1)
print(o1.total)
print(o1 == o2)
Example explained
Line 1total is declared with init=False, so it is a real field but never a constructor parameter; something must assign it.
Line 2__post_init__ runs at the end of the generated __init__, which is the only hook where derived values can be computed.
Line 3repr=False drops note from the generated __repr__, so the printed line shows three fields, not four.
Line 4compare=False leaves note out of the field tuple used by __eq__, so two orders differing only in note compare equal.
Important notes
frozen=True only blocks rebinding attributes; a list or dict stored in a field can still be mutated in place, and hashing such an instance raises TypeError because the list itself is unhashable.
A bare `total = 0.0` without an annotation is not a field: it stays a class attribute and is missing from __init__, __repr__, and __eq__.
Common mistakes
Writing `tags: list = []` instead of using default_factory: the class body raises ValueError: mutable default <class 'list'> for field tags is not allowed.
Assuming the annotations are enforced: Point("a", "b") is constructed happily and only fails later inside arithmetic, because @dataclass never checks types at runtime.
Putting a plain dataclass into a set or using it as a dict key: the generated __eq__ sets __hash__ to None, so you get TypeError: unhashable type, and the fix is frozen=True.
Try it yourself
Change, predict, then run
Write a frozen, ordered dataclass Card with fields rank: int and suit: str, then build a list of three cards, print sorted(cards), and print len(set(cards)) after adding a duplicate card.
Open the Python workspaceCheck your understanding
A frozen dataclass has a field holding a list. You append an item to that list. What happens?
- The append succeeds, because frozen only prevents rebinding attributes, not mutating the object a field points to
- FrozenInstanceError is raised, because every operation on a frozen instance is blocked
- TypeError is raised, because frozen dataclasses convert list fields to tuples
- The append succeeds but silently operates on a copy, leaving the instance unchanged
Show answer
frozen=True works by generating a __setattr__ (and __delattr__) that raise FrozenInstanceError, so only statements like obj.field = value fail; obj.field.append(x) never touches __setattr__ and mutates the existing list. FrozenInstanceError is tempting because frozen sounds like immutable, but the freeze applies to the instance's attribute bindings, not to the objects those attributes reference, and no conversion or copying of field values takes place.