Mutable defaults are shared state

The classic default-list bug and the sentinel pattern that prevents it

The classic Python gotcha: `def add_user(name, users=[])` creates the empty list once at function definition time, not on each call. That list is stored in `__defaults__` and reused every time the caller omits that argument. All subsequent calls share and mutate the same list object. The fix is the sentinel pattern: use `None` as the default, check with `is None`, and create a fresh container inside the function body. This is not a Python quirk. It follows directly from how the language evaluates default arguments at definition time. The same issue affects dataclasses via `field(default_factory=list)`, and dict patterns like `setdefault` where the default value is evaluated eagerly. <a href="/language-parameters">Learn how Python evaluates and stores default arguments</a>. <a href="/dict-setdefault">See the same eager-evaluation issue in dict patterns</a>.

Understand.
Visualize.
Master.

Python in Depth

An interactive engineering reference for Python internals

Quick note

Definition time is the trap.

:)
Python version

Targets Python 3.10–3.14. Python 3.9 and below are End-of-Life.

TABLE OF CONTENTS
2.3Mutable defaults are shared state

The classic default-list bug and the sentinel pattern that prevents it

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: str
def append_bad(event: AuditEvent, bucket: list[AuditEvent] = []) -> list[AuditEvent]:
bucket.append(event)
return bucket
print(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 dataclass
from 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 events
print(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 attempt
first = 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.

Further reading

  • Official FAQ: why defaults are shared
  • Official docs: function definitions
  • Official docs: dataclass default factories
  • PEP 671: late-bound defaults draft
  • CPython source: function objects
BOARD NOTESContext
WHY NO BENCHMARK?

This topic is better taught with structure, semantics, and cross-references than with a synthetic chart.

Definition time is the trap.

RELATED GUIDES
NEXT CHECKS
Contribute