Run it
The accumulation, the shared object read straight off the function, and the two-line fix - plus the same bug wearing a clock.
The accumulation, and the shared object read straight off the function.
The fix is two lines, and a private sentinel covers the case where None is itself a legitimate value.
The same rule with no list in sight, which is how it reaches production.
Defaults are evaluated once
A def statement is executable code. When it runs, Python evaluates the default expressions and stores the resulting objects on the function — you can read them back from f.__defaults__. They are not re-evaluated per call, because there is nothing left to evaluate.
For an immutable default that distinction is invisible: sharing one 0 or one "" between calls has no consequences, because nothing can change it. For a list, a dict, a set or a class instance it is the whole bug, because the shared object accumulates every mutation any call makes.
The sentinel, and why None
The fix is two lines:
def collect(item, into=None):
if into is None:
into = []
Now the list is built by the body, so each call gets its own. None is the conventional sentinel because it is a singleton and is None is unambiguous.
Use a private sentinel object instead when None is a legitimate value a caller might pass — _MISSING = object(), then if into is _MISSING. That distinction matters for wrappers and configuration functions where "not given" and "given as None" mean different things.
Where it actually bites
Almost never with a literal [], because that is the version everybody has been warned about. It bites through things that do not look like defaults at all:
def f(when=datetime.now()) freezes one timestamp at import time and returns it forever, which is the same bug wearing a clock. def f(cfg={}) in a class body gives every instance the same dictionary. And a default whose value comes from a module-level mutable — def f(opts=DEFAULT_OPTS) — hands callers a reference they can mutate for everyone.
The general rule that covers all of them: a default should be an immutable value, or None.
Why the language does not just fix it
Re-evaluating defaults per call would make every call pay for the expression, and it would make the value depend on when the call happened rather than on the definition — which is its own class of surprise. Early binding is also what makes the lambda i=i: i trick work for late-bound closures, where evaluating once at definition time is exactly the behaviour you want. It is a consistent rule that is wrong for one kind of value, rather than an oversight.
What to say out loud
Defaults are evaluated once at definition time, so a mutable default is one shared object for the life of the function. The second call sees what the first one appended. I use None as the sentinel and create the list inside the function.
Then stop. The commonest failure on a question like this is answering it correctly in one sentence and then talking for another minute until something wrong comes out.
What to notice while it runs
- The list exists before any call — it is stored on
f.__defaults__.
- Each call appends to the same object, so the return value grows.
- The
None version builds a new list per call, which is what the caller expected all along.
Edge cases to raise
Volunteering these is most of what separates a correct answer from a good one.
Passing your own argument leaves the shared default untouched, which is exactly why the bug survives testing: any test that supplies the list never sees it.
The shared object is per function, not per module or per instance. A method with a mutable default shares one object across every instance of the class, which looks like a class attribute bug and is not.
Mutating an argument the caller passed is the same class of surprise, without any default involved. If a function appends to a list it was given, say so in the name or the docstring - or copy on the way in.
The follow-ups interviewers ask
"How would you catch this in code review?" Any mutable literal in a signature: [], {}, set(), or a call that builds one. Linters flag it (B006 in flake8-bugbear), which is the honest answer - you catch it with a tool rather than with vigilance.
"What do dataclasses do about it?" They refuse. field: list = [] raises ValueError: mutable default at class creation, and you write field: list = dataclasses.field(default_factory=list) instead. That is the same fix as the None sentinel, made mandatory - and a good thing to cite, because it shows the language designers agreed it was a trap.
"Is a default that calls a function also evaluated once?" Yes. def f(t=time.time()) and def f(conn=connect()) both run once, at definition time, which is how a stale timestamp or a connection opened at import ends up in production.
Common wrong answers
"Python caches the return value of the function." It caches nothing. One default object is created once and shared; the function runs fully every call.
"Use a tuple instead." It removes the symptom by making mutation impossible, and it changes the type the caller gets. Correct answer is None plus a build in the body.
"It is a bug in Python." It is consistent early binding, and the same rule is the standard fix for late-bound closures. Calling it a bug suggests you have not seen where the behaviour is useful.
Recap in one screen
- The list exists before any call - it is stored on f.__defaults__.
- Each call appends to the same object, so the return value grows.
- The None version builds a new list per call, which is what the caller expected all along.
- The one-line answer: Because the default is evaluated once, when the def statement runs - not on each call.
- Worth trying: Call collect("z", []) between two bare calls. Passing your own argument leaves the shared default untouched, which is why the bug hides in tests that always pass one.
- Worth trying: Change the default to into=() and watch it raise instead. An immutable default cannot accumulate - it can only fail loudly, which is better.