Mutable default arguments: the value lives in a field of the function, and every call shares it
Everyone knows the rule about not writing a list as a default argument, and everyone breaks it anyway, because it reads like an arbitrary prohibition. It stops being arbitrary the moment you see where that value lives: in the function's __defaults__, created once when the def executed.
Full technical treatment
TL;DR
The expression after = is evaluated once — when the def executes, not
when the function is called. The resulting object is kept by the function and
handed to every call in which that argument is not passed explicitly. Pass
your own value and the stored object plays no part. But if the object is mutable
and the function mutates it, the changes stay in it and reach the next call:
that is exactly where the rule about lists in default arguments stops being an
arbitrary prohibition.
Hence the main consequence: the function accumulates state nobody set up —
and that state is visible from outside. The stored value lives in the
function's __defaults__ field (for keyword-only parameters, in
__kwdefaults__), it can be read, and the list in it grows from call to call.
The same moment of evaluation gives a second trap with no mutable object in
sight: def log(at=time.time()) freezes the module's import time forever, and
def f(x=CONST) takes the value of CONST as of the definition and never
notices a reassignment. The fix is None plus an explicit check; a sentinel of
your own is needed exactly where None is a legitimate argument value.
| which defaults | where they live | in what |
|---|---|---|
| positional | __defaults__ | a tuple |
| keyword-only | __kwdefaults__ | a dict |
Beyond that is what separates knowing from having read. The cost of doing it
right: 48.78 ns against 32.67 (3.13.7). But more than two thirds of that
difference is creating the new list — the very work the correct version exists
to do; the check itself costs 4.74 ns. Dataclasses forbid the mistake outright:
x: list = [] raises ValueError while the class is being defined — though the
check's boundary runs along hashability rather than mutability. And one
measurement came out against expectation: a warmed-up lru_cache beats a
hand-rolled cache in a default argument by 1.43 times — 46.26 ns against 66.09.
- a parameter can have a default value, and then the argument may be left out of the call;
- a list or a dictionary can be changed without creating a new object, while a number or a string cannot;
defis a line of the program that runs, not a declaration the compiler merely takes note of.
__defaults__,__kwdefaults__, a sentinel object in place ofNone;- dataclasses and
default_factory,functools.lru_cache.
Base: the function remembers what it has no business remembering
The place to start is the bug everyone recognises on sight. A function puts what it is given into a list and returns it:
def acc(item, into=[]):
into.append(item)
return into
print(acc(1)) # [1]
print(acc(2)) # [1, 2] — and [2] was expectedThe second call returned a list of two items although it was given one. Hence the rule everybody memorises: never write a list as a default argument.
But in that form the rule cannot help: it reads like an arbitrary prohibition,
which is exactly why it gets broken. The question this lesson answers is not
"what is forbidden" but when the thing after the equals sign is evaluated.
If it were evaluated on every call, the behaviour above would be inexplicable.
If it is evaluated once, when the def runs, then everything is explicable —
including the cases with no lists in them at all.
The second is what happens: the value is evaluated once, and the resulting object is handed to every call in which that argument is not passed. The list does not vanish between calls, because there is only one of it.
That is already enough to answer the basic interview question. Everything below is about where exactly that object lives and how to see it, what else breaks for the same reason, and what the correct version really costs.
Mechanism 1: where the default value lives
def runs. That is a rule of the language, not a CPython quirk.The reference describes the mechanism in two sentences — and that is enough to derive everything else.
Default parameter values are evaluated from left to right when the function
definition is executed. This means that the expression is evaluated once, when
the function is defined, and that the same "pre-computed" value is used for each
call.
That "pre-computed value" is not an abstraction — you can pick it up by hand:
def acc(item, into=[]):
into.append(item)
return into
print(acc.__defaults__) # ([],)
acc(1)
acc(2)
print(acc.__defaults__) # ([1, 2],)The function's field grows along with the list. There is no special magic in
the bug: the list was created once, put into the field, and handed out from
there to every call in which that argument is not passed explicitly. A call
like acc(3, []) takes no part in this picture at all — it works on its own
list, and the field stays as it was.
Mechanism 2: the same mechanism, a second trap
Mutability is the half everyone remembers; the moment of evaluation is the half they forget. Both come out of the same moment.
The default values are evaluated at the point of function definition in the
defining scope.
TIMEOUT = 5
def fetch(url, timeout=TIMEOUT):
...
TIMEOUT = 30 # fetch will still take 5And the most expensive manifestation:
import time
def log(message, at=time.time()):
... # at is the module's import time, foreverSuch a function never breaks and never raises anything. It simply writes the same timestamp into every record, and nobody notices until someone actually needs the logs.
Mechanism 3: the fix, and when None will not do
The reference offers the solution immediately:
def acc(item, into=None):
if into is None:
into = []
into.append(item)
return intoThe function's field then stays (None,) forever — None is immutable, there
is nothing in it to spoil.
But None does not always fit. If None is a legitimate argument value,
the check cannot tell "not passed" from "passed None":
MISSING = object()
store = {"k": None} # the dictionary being searched
def get(key, default=MISSING):
value = store.get(key, MISSING)
if value is MISSING:
if default is MISSING:
raise KeyError(key)
return default
return valueHere get("k", None) must return None while get("k") must raise
KeyError, and only a sentinel of your own can tell them apart. The sentinel costs 8.07
ns more than None — not because of the comparison but because of the load:
None compiles to a constant, while the sentinel lives among the module's
globals. That is not, of course, why it gets chosen: it gets chosen because
None is taken in such a function.
Mechanism 4: when it is done on purpose
The trick does have one use: a dictionary in a default argument is state that survives between calls.
def fib(n, _cache={0: 0, 1: 1}):
if n not in _cache:
_cache[n] = fib(n - 1) + fib(n - 2)
return _cache[n]It works. But it has three problems, and the third came as a surprise.
The first two are obvious: the cache can only be cleared from outside through
fib.__defaults__, and anyone who calls fib can substitute the cache with a
second argument — by accident or on purpose.
The third is speed, and the expectation here was the opposite. The
hand-rolled cache seemed bound to beat functools.lru_cache: that one has a
wrapper and this one does not. On a warmed-up cache it came out the other way
round:
| cache | ns per hit |
|---|---|
| a dictionary in a default argument | 66.09 |
functools.lru_cache | 46.26 |
lru_cache is 1.43 times faster. The reason is that its wrapper is written in
C and does one dictionary lookup, while the Python body does two — the not in check and the read — plus the function call itself. The C wrapper turned out cheaper
than an extra lookup plus a Python-level call.
Mechanism 5: the same thing in classes
The mechanism is the same, and the dataclasses documentation opens its explanation with exactly that: "Python stores default member variable values in class attributes."
class C:
items = [] # one list for all instances
o1, o2 = C(), C()
o1.items.append(1)
print(o2.items) # [1]Dataclasses are the one automatic check for this mistake I know of in the standard library. Its boundary is narrower than it looks:
@dataclass
class Bad:
items: list = []
# ValueError: mutable default <class 'list'> for field items
# is not allowed: use default_factoryThe check sits in the decorator itself and fires when the class is defined.
But it has a boundary of its own: it goes by hashability, not by
mutability. list, dict and set are forbidden, while a class of your own
with mutable state passes silently — and the instances will share one object.
In an ordinary function there is no such check at all: only linters do it.
Deeper: what it costs
One argument in defence of a mutable default is still standing: "at least it is faster without the check". The argument is true: 32.67 ns against 48.78, that is, 49% more. The breakdown shows what exactly is being paid for:
- 11.37 ns — creating the new list. This is not overhead but the work the correct version exists to do: the broken one is cheaper precisely because it creates no list.
- 4.74 ns — the
is Nonecheck itself.
So the real price of the check itself is five nanoseconds per call. The other eleven are the new list the broken version simply does not create.
And these numbers have the boundary any measurement has: one machine, one version. What carries over is not "49%" but the breakdown — what that difference is made of, and why comparing the two versions on speed is not a fair comparison at all: they return different things.
How to answer in an interview
The short answer: the expression after = is evaluated once, when the def
executes, and the resulting object is handed to every call in which that
argument is not passed. That is why a mutable default accumulates changes
between calls. The fix is a None check; a sentinel of your own is needed only
where None is a legitimate argument value.
That is enough for a correct answer. What follows is what you add when the interviewer digs.
If the interviewer digs deeper
What makes an answer good: showing f.__defaults__ — that turns the rule into a
consequence. The value lives in a field of the function, the field is visible
from outside, and the list in it grows from call to call; after that the rule
does not have to be remembered, it can be derived.
And naming the other half of the mechanism: the value is evaluated at def
time, which is why def log(at=time.time()) breaks for the same reason with no
mutable object involved at all.
The phrasing itself is worth keeping precise. Not "one value for all calls" but the same object handed to every call in which that argument is not passed explicitly: pass your own value and the function's field plays no part at all. The bug lives neither in the list nor in the call but at their intersection.
Next they ask
You fixed it with None. Does that always work?
Not always: None works as "no value was passed" only while None is not itself
a valid value for the parameter. Where it is valid you need a separate sentinel
object, otherwise the fix quietly changes what a call means.
And if that behaviour is wanted on purpose?
Then it is used on purpose — it is a way to attach state to a function, and it is legitimate. The difference is not in the mechanism but in the intent: the same behaviour is sometimes a technique and sometimes a bug, and only the author's awareness separates them.
Same thing in classes?
Yes, and the trap there is the same by mechanism: the value is computed once, when the declaration runs, and from then on it is shared by everyone who has not overridden it.
Common misconceptions
The default value is evaluated on every call
Once, when the def executes. Verified with a counter: a function standing in a default value is called exactly once at definition, however many times the outer function is called afterwards. Hence def log(at=time.time()), freezing the module's import time.
The problem is lists and dictionaries
The problem is mutability, not the type. The same def f(x=MyClass()) breaks the same way if the object has mutable state. And conversely, def f(x=()) or def f(x=0) are safe not because they are “simple types” but because there is nothing in them to spoil.
The correct version is noticeably more expensive: it has to check is None every time
The check costs 4.74 ns. The gap between the broken and the correct version is 16 ns, and more than two thirds of it goes on creating the new list — work the broken version never does at all. Comparing them on speed is not meaningful in the first place: they return different things.
A sentinel of your own instead of None is needless complication
It is needed in exactly one case, and that case is real: when None is a legitimate argument value. The None check then cannot tell “not passed” from “passed None”, and a function like get(key, default=None) stops distinguishing “no such key” from “the default is None”.
A cache in a default argument beats lru_cache: that one has a wrapper
Measured: 66.09 ns against 46.26 on a warmed-up cache — lru_cache is 1.43 times faster. The expectation was the opposite. Its wrapper is written in C and does ONE dictionary lookup, while the Python body does two — the not in and the read — plus the function call itself.
Version history
| Version | Change | What this means for your code |
|---|---|---|
| 3.0 | Keyword-only parameters arrive, and with them a second field — __kwdefaults__. The mechanism is the same: the dictionary is created once when the def executes, and a mutable value inside it behaves exactly as one in __defaults__. | |
| 3.7 | Dataclasses arrive (PEP 557) — and with them an automatic check that catches this mistake (in the cases described above: the boundary runs along hashability). x: list = [] in a dataclass body raises ValueError when the CLASS is defined, not on the first call. The message text is identical on 3.11–3.14. | |
| 3.11 | The lesson's baseline. The contents of __defaults__ before and after the calls, the moment the value is evaluated and the text of the dataclass error are the same on 3.11 as on 3.14.7 — verified by running one script on four versions. The mechanism has not changed and is not going to: it is described in the language reference, not in implementation details. |
Practice
Two exercises. Answer first, then check against the real output: in both, the right answer comes from a recorded run rather than from an editor.
Practice · predict the output
def add(item, bucket=[]): bucket.append(item) return bucket print(add(1)) print(add(2)) print(add.__defaults__)
Practice · estimate
Check yourself
def acc(item, into=[]) is called three times: acc(1), acc(2), acc(3). What does the third call return, and what is acc.__defaults__?
What measured this
The numbers in this article come from these scripts. Each one opens from here, together with the record of the run: what it was measured on, what came out, and with what spread.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- The expression after
=is evaluated once — when thedefexecutes, not when the function is called. The resulting object is kept by the function and handed to every call in which that argument is not passed explicitly. Pass your own value and the stored object plays no part. But if the object is mutable and the function mutates it, the changes stay in it and reach the next call: that is exactly where the rule about lists in default arguments stops being an arbitrary prohibition. - Hence the main consequence: the function accumulates state nobody set up — and that state is visible from outside. The stored value lives in the function's
__defaults__field (for keyword-only parameters, in__kwdefaults__), it can be read, and the list in it grows from call to call. The same moment of evaluation gives a second trap with no mutable object in sight:def log(at=time.time())freezes the module's import time forever, anddef f(x=CONST)takes the value ofCONSTas of the definition and never notices a reassignment. The fix isNoneplus an explicit check; a sentinel of your own is needed exactly whereNoneis a legitimate argument value. - | which defaults | where they live | in what | | --- | --- | --- | | positional |
__defaults__| a tuple | | keyword-only |__kwdefaults__| a dict | - Beyond that is what separates knowing from having read. The cost of doing it right: 48.78 ns against 32.67 (3.13.7). But more than two thirds of that difference is creating the new list — the very work the correct version exists to do; the check itself costs 4.74 ns. Dataclasses forbid the mistake outright:
x: list = []raisesValueErrorwhile the class is being defined — though the check's boundary runs along hashability rather than mutability. And one measurement came out against expectation: a warmed-uplru_cachebeats a hand-rolled cache in a default argument by 1.43 times — 46.26 ns against 66.09.
In fact
- Once, when the
defexecutes. Verified with a counter: a function standing in a default value is called exactly once at definition, however many times the outer function is called afterwards. Hencedef log(at=time.time()), freezing the module's import time. - The problem is mutability, not the type. The same
def f(x=MyClass())breaks the same way if the object has mutable state. And conversely,def f(x=())ordef f(x=0)are safe not because they are “simple types” but because there is nothing in them to spoil. - The check costs 4.74 ns. The gap between the broken and the correct version is 16 ns, and more than two thirds of it goes on creating the new list — work the broken version never does at all. Comparing them on speed is not meaningful in the first place: they return different things.
- It is needed in exactly one case, and that case is real: when
Noneis a legitimate argument value. TheNonecheck then cannot tell “not passed” from “passed None”, and a function likeget(key, default=None)stops distinguishing “no such key” from “the default is None”. - Measured: 66.09 ns against 46.26 on a warmed-up cache —
lru_cacheis 1.43 times faster. The expectation was the opposite. Its wrapper is written in C and does ONE dictionary lookup, while the Python body does two — thenot inand the read — plus the function call itself.
By version
- 3.0
- Keyword-only parameters arrive, and with them a second field —
__kwdefaults__. The mechanism is the same: the dictionary is created once when thedefexecutes, and a mutable value inside it behaves exactly as one in__defaults__.< - 3.7
- Dataclasses arrive (PEP 557) — and with them an automatic check that catches this mistake (in the cases described above: the boundary runs along hashability).
x: list = []in a dataclass body raisesValueErrorwhen the CLASS is defined, not on the first call. The message text is identical on 3.11–3.14.< - 3.11
- The lesson's baseline. The contents of
__defaults__before and after the calls, the moment the value is evaluated and the text of the dataclass error are the same on 3.11 as on 3.14.7 — verified by running one script on four versions. The mechanism has not changed and is not going to: it is described in the language reference, not in implementation details.<
What is covered
- Base: the function remembers what it has no business remembering
- Mechanism 1: where the default value lives
- Mechanism 2: the same mechanism, a second trap
- Mechanism 3: the fix, and when `None` will not do
- Mechanism 4: when it is done on purpose
- Mechanism 5: the same thing in classes
- Deeper: what it costs
- How to answer in an interview
- Next they ask
- Common misconceptions
- Version history
- Practice
- Check yourself
- What measured this
Sources & further reading
5 SOURCES
- Language reference — function definitions, default valuesOfficial documentation. The primary source, and it describes the mechanism, the mistake and the way around it in one paragraph: «Default parameter values are evaluated from left to right when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call». And directly on mutable objects: «if the function modifies the object (e.g. by appending an item to a list), the default parameter value is in effect modified. This is generally not what was intended».https://docs.python.org/3.14/reference/compound_stmts.html#function-definitions
- Data model — the function's __defaults__ attributeOfficial documentation. Where the value actually lives: «A tuple containing default parameter values for those parameters that have defaults, or None if no parameters have a default value». Next to it is __kwdefaults__, a separate field for keyword-only parameters. Reading these fields is what turns the rule into a consequence: the list from __defaults__ is visible from outside and grows from call to call.https://docs.python.org/3.14/reference/datamodel.html#special-read-only-attributes
- Tutorial — defaults are evaluated in the defining scopeOfficial documentation. The other half of the same mechanism, remembered less often: «The default values are evaluated at the point of function definition in the defining scope». The same page gives the example with i = 5 followed by i = 6 where the function prints 5. This is where def log(at=time.time()) comes from.https://docs.python.org/3.14/tutorial/controlflow.html#default-argument-values
- dataclasses — mutable default valuesOfficial documentation. The one automatic check for this mistake known in the standard library; its boundary runs along hashability, not mutability. The documentation opens the explanation with the same mechanism in classes: «Python stores default member variable values in class attributes», gives an example where o1.x is o2.x, and shows that a dataclass raises ValueError on such a declaration. The same page on default_factory: «it must be a zero-argument callable that will be called when a default value is needed for this field».https://docs.python.org/3.14/library/dataclasses.html#mutable-default-values
- functools.lru_cacheOfficial documentation. The standard replacement for the one sensible use of this trick — a cache held in a default argument. It is here for the measurement: a warmed-up lru_cache turned out to be 1.43 times faster than the hand-rolled cache, even though it has a wrapper and the hand-rolled one does not.https://docs.python.org/3.14/library/functools.html#functools.lru_cache