promptdojo_

@dataclass — the class shape AI ships in every modern Python project — step 3 of 9

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.