Mastery
Mastery/Python/A. Object model, identity & memory
T1 · high-leverage

The mutable default argument trap

A def's default argument value is evaluated once, at function-definition time — not once per call. If that default is a mutable object (list, dict, set), every call that doesn't override it shares the same object, so mutations leak across calls. This is one of the most common real bugs in beginner and even intermediate Python code, and it's also why idiomatic code uses None as the sentinel default instead.

python
def append_item(item, bucket=[]):
    bucket.append(item)
    return bucket

print(append_item(1))          # [1]
print(append_item(2))          # [1, 2]  <- same list object as before!
print(append_item(3, bucket=[]))  # [3]   <- explicit fresh list breaks the sharing
print(append_item(4))          # [1, 2, 4] <- back to sharing the original default

The correct idiom: default to None, create the mutable object inside the function body.

python
def append_item_fixed(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

print(append_item_fixed(1))
print(append_item_fixed(2))   # independent list this time

Interview angle

Extremely common as a "what's wrong with this function?" question, and a genuinely good one — it cleanly separates candidates who've actually been bitten by this in real code from those who've only read about Python. The strong follow-up is "why does Python behave this way?" (defaults are evaluated once, at def-time, and stored on the function object itself) — that "why" is what lets someone reason about related surprises, like default arguments that call a function at def-time instead of per-call.

In the industry

Every mainstream Python linter flags this automatically (pylint's dangerous-default-value, ruff's B006), so it rarely survives code review on a team with CI lint gates — but it's still one of the most common bugs in scripts, notebooks, and legacy code without linting. It's consistently one of the first things a senior engineer points out reviewing a junior contributor's first pull request, and the None-sentinel fix (def f(x=None): x = x or []) is idiomatic enough that seeing the raw mutable-default version is itself a weak signal about a codebase's review rigor.