Why does this default argument remember?

Because the default is evaluated once, when the def statement runs — not on each call. One list is created, attached to the function object, and reused by every call that does not pass its own, so mutations accumulate across calls. The fix is None as the default and build the real value inside the body.

Overview

The question, and what it is testing

Because the default is evaluated once, when the def statement runs — not on each call. One list is created, attached to the function object, and reused by every call that does not pass its own, so mutations accumulate across calls. The fix is None as the default and build the real value inside the body.

Interviewers use this one because the answer separates people who have read the language from people who have only used it. Nothing here is obscure; all of it is observable, which is what the editor below is for.

Python semanticsConceptualMedium

Step through it

What to watch

  • 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.

Say this 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."

Why does this default argument remember?

What does def f(x, acc=[]) do on the second call, and why?

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.

1Python
Output

The fix is two lines, and a private sentinel covers the case where None is itself a legitimate value.

2Python
Output

The same rule with no list in sight, which is how it reaches production.

3Python
Output

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.

How the code works

The accumulation, the shared object read straight off the function, and the two-line fix - plus the same bug wearing a clock.

How the code works

  1. into=[]Evaluated when the def executes. From then on there is exactly one list, owned by the function.
  2. collect.__defaults__The shared object, readable from outside. Printing it is what turns "mysterious behaviour" into "an object with a value".
  3. if into is None:The sentinel check. is rather than == because None is a singleton and a caller's object might define a surprising __eq__.
  4. at=time.time()The same rule with no list in sight. One timestamp, captured at definition time, returned by every call — which is how this bug reaches production.

Change one thing

  • 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.
  • Change the default to into=() and watch it raise instead. An immutable default cannot accumulate — it can only fail loudly, which is better.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 4

Answer without scrolling back up.

  1. When is a default argument expression evaluated?

  2. Why is def f(x=0) harmless while def f(x=[]) is not?

  3. The conventional fix is:

  4. Why prefer a private sentinel over None in some APIs?

Cheat sheet

Why does this default argument remember?

Because the default is evaluated once, when the def statement runs — not on each call. One list is created, attached to the function object, and reused by every call that does not pass its own, so mutations accumulate across calls. The fix is None as the default and build the real value inside the body.

INTERVIEW · vizlearn.in/interview/the-mutable-default-argument.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.