@dataclass — the class shape ai ships in every modern python project
default_factory and frozen=True — the two flags AI uses constantly
The intro covered the basic dataclass. Two more shapes you'll see in real AI code, both of which solve real problems.
default_factory — the right way to default to an empty list
Try this:
@dataclass
class Cart:
items: list = []
Python raises a ValueError at class-definition time:
ValueError: mutable default <class 'list'> for field items is not allowed
This is the "shared mutable" footgun from the previous lesson.
Dataclasses refuse to let you make that mistake — if every Cart()
shared the same items list, you'd hate your life.
The fix is field(default_factory=...). You import field from the
same module and pass a callable that gets called to produce a
fresh default each time:
from dataclasses import dataclass, field
@dataclass
class Cart:
owner: str
items: list = field(default_factory=list)
list (the type itself, no parens) is callable — calling it
returns a new empty list. Every Cart() call produces its own.
This is the shape AI ships when it remembers — and you'll see it
constantly in any class that holds a list, dict, or set.
For a non-empty default, pass a lambda:
items: list = field(default_factory=lambda: ["welcome"])
For a dict:
config: dict = field(default_factory=dict)
Anywhere you'd reach for [], {}, or set() as a dataclass
default, the right shape is field(default_factory=...).
frozen=True — make the dataclass immutable
By default, dataclass fields are mutable — user.age = 30
reassigns the attribute and Python is fine with it. Sometimes you
want the opposite: a value object that, once constructed, can never
change. frozen=True does that:
@dataclass(frozen=True)
class Coord:
x: int
y: int
origin = Coord(0, 0)
origin.x = 5 # raises FrozenInstanceError
Two things this buys you. Bug prevention — code that should never be modifying these objects can't, by accident. Hashability — frozen dataclasses are hashable, which means you can use them as dict keys or put them in sets. Mutable dataclasses can't.
When AI writes a dataclass for "a value that represents a point in
time / a coordinate / an identifier / a config snapshot", reach for
frozen=True. When the class is genuinely a workspace that gets
mutated (a cart, a buffer, a queue), leave it mutable.
A worked example
The editor on the right has both flags in action:
from dataclasses import dataclass, field
@dataclass
class Cart:
owner: str
items: list = field(default_factory=list)
alex = Cart("alex")
sam = Cart("sam")
alex.items.append("apple")
sam.items.append("bread")
print(alex) # Cart(owner='alex', items=['apple'])
print(sam) # Cart(owner='sam', items=['bread'])
Each cart gets its own list. The default_factory=list runs per
instance, generating a fresh empty list each time.
@dataclass(frozen=True)
class Coord:
x: int
y: int
origin = Coord(0, 0)
origin.x = 5 # blocked
The assignment fails with a FrozenInstanceError, which the example
catches and prints. You can't accidentally mutate a frozen
dataclass.
Where AI specifically gets this wrong
Two patterns to flag.
One: items: list = [] — the literal-mutable-default shape.
This raises at class-definition time, but Cursor writes it anyway,
sees the error, and sometimes "fixes" it by removing the default
entirely (forcing the caller to pass an empty list every time —
ugly). The right fix is field(default_factory=list). Always.
Two: forgetting frozen=True on identifier-like dataclasses.
If you read a UserId, OrderId, Coord, RequestKey, or any
"this is a value, not a workspace" dataclass that doesn't have
frozen=True, that's a soft bug — code can mutate the value out
from under you. Add the flag.
Run the editor. Two carts with separate lists, one frozen coordinate that refuses to be mutated.