Deep Engineering
Intermediate·Published·3.11 · 3.12 · 3.13 · 3.14·25 MIN

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 defaultswhere they livein 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.

Where to start
Before this lesson it is enough to understand
  • 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;
  • def is a line of the program that runs, not a declaration the compiler merely takes note of.
You do not need to know in advance
  • __defaults__, __kwdefaults__, a sentinel object in place of None;
  • 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:

PYTHON
def acc(item, into=[]):
    into.append(item)
    return into
 
print(acc(1))       # [1]
print(acc(2))       # [1, 2] — and [2] was expected

The 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

language contractLanguage guarantee: a default expression is evaluated once, when the 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.

Language reference, function definitions

That "pre-computed value" is not an abstraction — you can pick it up by hand:

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

Tutorial, default argument values
PYTHON
TIMEOUT = 5
 
def fetch(url, timeout=TIMEOUT):
    ...
 
TIMEOUT = 30        # fetch will still take 5

And the most expensive manifestation:

PYTHON
import time
 
def log(message, at=time.time()):
    ...            # at is the module's import time, forever

Such 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:

PYTHON
def acc(item, into=None):
    if into is None:
        into = []
    into.append(item)
    return into

The 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":

PYTHON
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 value

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

PYTHON
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:

cachens per hit
a dictionary in a default argument66.09
functools.lru_cache46.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."

PYTHON
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:

PYTHON
@dataclass
class Bad:
    items: list = []
# ValueError: mutable default <class 'list'> for field items
#             is not allowed: use default_factory

The 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

measured observationbench/defaults/cost.py, CPython 3.13.7. The difference between a sentinel and a mutable default is nanoseconds; the choice is not made on those.

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 None check 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

Next they ask

You fixed it with None. Does that always work?

Short answer

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.

Next they ask

And if that behaviour is wanted on purpose?

Short answer

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.

Next they ask

Same thing in classes?

Short answer

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

Claim

The default value is evaluated on every call

Actually

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.

Claim

The problem is lists and dictionaries

Actually

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.

Claim

The correct version is noticeably more expensive: it has to check is None every time

Actually

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.

Claim

A sentinel of your own instead of None is needless complication

Actually

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

Claim

A cache in a default argument beats lru_cache: that one has a wrapper

Actually

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

VersionChangeWhat this means for your code
3.0Keyword-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.7Dataclasses 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.11The 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

The default value is an empty list. The function is called twice, then __defaults__ is printed. What does this code print?
def add(item, bucket=[]):
  bucket.append(item)
  return bucket


print(add(1))
print(add(2))
print(add.__defaults__)

Practice · estimate

A cache of squares written as a dict in a default argument, against functools.lru_cache — both warm. How many times FASTER is lru_cache?
times

Check yourself

Question 1 of 5

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.

Sources & further reading

5 SOURCES

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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