Dataclasses, enums & typing basics
Model data with @dataclass, name fixed choices with Enum, and add type hints.
Model your data, don't hand-write it
Real backends move records around: a User, an Order, an API payload. If you write those as plain classes, you hand-write an __init__ to store fields, a __repr__ so logs are readable, and an __eq__ so two equal records compare equal. That is boilerplate you will get subtly wrong (forget one field in __eq__ and dedup silently breaks). @dataclass generates all three from a typed field list, so the class is the schema.
Dataclasses: fields become the constructor
Each name: type line under a @dataclass is a field. The decorator reads those fields and writes __init__, __repr__, and __eq__ for you, in field order.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
print(Point(1, 2)) # Point(x=1, y=2)
print(Point(1, 2) == Point(1, 2)) # True
The generated __eq__ compares instances field by field, which is exactly what the Apply exercise leans on: two points built from the same coordinates are equal because their (x, y) tuples are equal.
Which methods the decorator writes depends on the flags you pass it:
| You write | Generates | Instances are |
|---|---|---|
| @dataclass | __init__, __repr__, __eq__ | mutable, and NOT hashable |
| @dataclass(frozen=True) | the above plus __hash__ | immutable, so usable as dict keys and in sets |
| @dataclass(order=True) | the above plus __lt__, __le__, __gt__, __ge__ | sortable, compared field by field |
| @dataclass(eq=False) | __init__ and __repr__ only | compared by identity, like a plain class |
Type hints describe intent, they do not enforce it
Annotations like x: int are metadata. Python does not check them at runtime; passing Point("a", "b") still constructs an object. Their value is for readers and tools like mypy or your editor, which flag mismatches before you run.
def total(prices: list[int]) -> int:
return sum(prices)
note: int | None = None # int or None; int | None is the modern Optional
Enums: name a fixed set of choices
When a field has a small closed set of valid values (a status, a color, a role), a bare string invites typos like "gren". An Enum names each choice once. Members carry a .value, and you can look one up by name.
from enum import Enum
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
print(Color.RED.value) # red
print(Color["RED"]) # Color.RED (look up by name)
That name lookup is what the Practice driver does: Color[name].value turns "RED" into "red".
Pitfall: mutable default fields
You cannot give a dataclass field a mutable default like [] or {} directly. Python evaluates the default once, so every instance would share one list. The dataclass machinery blocks it outright:
from dataclasses import dataclass, field
@dataclass
class Cart:
items: list = [] # ValueError at class creation time
@dataclass
class Cart:
items: list = field(default_factory=list) # correct: fresh list per instance
Use field(default_factory=list) (or dict, set) so each instance gets its own container.
Validating and deriving with __post_init__
The generated __init__ only assigns fields, so there is no obvious place to check them or to compute
a value from the others. __post_init__ is that place: the dataclass calls it immediately after the
constructor finishes.
from dataclasses import dataclass, field
@dataclass
class Order:
unit_price: float
quantity: int
total: float = field(init=False) # not a constructor argument
def __post_init__(self):
if self.quantity < 1:
raise ValueError("quantity must be at least 1")
self.total = self.unit_price * self.quantity
print(Order(2.5, 4)) # Order(unit_price=2.5, quantity=4, total=10.0)
print(Order(2.5, 0)) # ValueError: quantity must be at least 1
Two jobs, both worth naming. Validation keeps an invalid object from ever existing, which is far
easier to reason about than checking validity at each use site. Derived fields pair
__post_init__ with field(init=False), so total is computed rather than passed in and cannot
drift out of sync with the values it came from.
One trap: under frozen=True ordinary assignment raises FrozenInstanceError, so a derived field
has to be set through the back door:
@dataclass(frozen=True)
class Doubled:
x: int
doubled: int = field(init=False)
def __post_init__(self):
object.__setattr__(self, "doubled", self.x * 2) # frozen blocks self.doubled = ...
print(Doubled(21)) # Doubled(x=21, doubled=42)
Interview nuance: the generated __eq__ compares field values only when the other object is the same dataclass type; against anything else it returns NotImplemented, so Point(1, 2) == (1, 2) is False, not True. Equality here means "same type and same fields," which is why dataclasses are safe to put in a set or use as dict keys once you add frozen=True (that also generates __hash__).
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
print(Point(1, 2)) # Point(x=1, y=2)
print(Point(1, 2) == Point(1, 2)) # TrueApply
Your turn
The task this lesson builds to.
Make Point a @dataclass with two fields, x: int and y: int.
Dataclasses generate __init__ and __eq__ for you, so two points with the same coordinates
compare equal. run(1, 2, 1, 2) should return True.
3 hints and 3 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Define a Color enum with three members: RED = "red", GREEN = "green", and
BLUE = "blue".
The driver looks up Color[name] and returns its .value, so run("RED") should return
"red".
3 hints and 3 automated checks are waiting in the workspace.