Mutable defaults are not a special list bug. They are the visible consequence of Python evaluating default expressions once when a function object is created.
Core answer
If each call needs fresh mutable state, create it inside the function or use field(default_factory=...) for dataclass fields. Keep a mutable default only when shared state is explicit, documented, and tested.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclass@dataclass(frozen=True, slots=True)class AuditEvent: order_id: str message: strdef append_bad(event: AuditEvent, bucket: list[AuditEvent] = []) -> list[AuditEvent]: bucket.append(event) return bucketprint(append_bad(AuditEvent("ORD-1", "accepted")))print(append_bad(AuditEvent("ORD-2", "replayed")))Why this design exists
Early-bound defaults make function objects cheap to call and let a default value be inspected through __defaults__. They also make defaults useful for dependency capture and cache-like state when that is intentional. The price is that a mutable object keeps its identity across omitted arguments.
PEP 671 is a draft design for explicit late-bound defaults. It is useful context because it shows the tradeoff, but it does not change the Python 3.10-3.14 rule this project teaches.
Mechanics and CPython internals
CPython evaluates default expressions while executing the def statement and stores positional defaults on the function object. A call that omits the parameter reuses the stored reference. Mutation changes that object; rebinding a local parameter does not rewrite the default slot.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclassfrom typing import Final@dataclass(frozen=True, slots=True)class AuditEvent: order_id: str message: str_MISSING: Final = object()def append_safe(event: AuditEvent, bucket: object = _MISSING) -> list[AuditEvent]: if bucket is _MISSING: events: list[AuditEvent] = [] elif isinstance(bucket, list): events = bucket else: raise TypeError("bucket must be a list of AuditEvent values") events.append(event) return eventsprint(append_safe(AuditEvent("ORD-1", "accepted")))print(append_safe(AuditEvent("ORD-2", "replayed")))Complexity and tradeoffs
Appending to the shared list remains amortized O(1); the bug is lifetime, not insertion complexity. Creating a fresh empty list is constant space and time before growth, which is a trivial cost compared with leaking state between requests.
Idiomatic patterns and refactoring
For object fields, use a factory so each instance gets its own mutable collection. This keeps the data model aligned with the function-level rule.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclass, field@dataclass(slots=True)class DeliveryAttempt: order_id: str errors: list[str] = field(default_factory=list)def fail(attempt: DeliveryAttempt, reason: str) -> DeliveryAttempt: attempt.errors.append(reason) return attemptfirst = fail(DeliveryAttempt("ORD-1"), "timeout")second = fail(DeliveryAttempt("ORD-2"), "refused")print(first.errors)print(second.errors)Common mistakes and edge cases
None is a poor sentinel when None is a valid value. A unique object() sentinel preserves that distinction. Also do not confuse this problem with closure late binding: defaults are evaluated early, while captured names in closures are usually looked up when the inner function runs.
When to use / When NOT to use
Use immutable defaults freely. Use fresh mutable values inside the function or a dataclass default factory when calls or instances should be isolated.
Do not use a mutable default for request-scoped state, accumulator output, or per-instance records unless shared lifetime is the explicit contract.