Why a mutable default argument in Python is shared between calls
A function with def f(x, into=[]) does not get a fresh list on each call. It
gets the same one, every time, for the lifetime of the program. Here is why,
and what to do instead.
The surprise
def collect(item, into=[]):
into.append(item)
return into
collect('a') : ['a']
collect('b') : ['a', 'b']
collect('c') : ['a', 'b', 'c']
Three independent-looking calls, one list.
Where the list actually lives
The rule is that default values are evaluated once, when the def statement
runs — not on each call. The result is stored on the function object, and you
can look at it:
collect.__defaults__ : (['a', 'b', 'c'],)
id of the default : 135662073621888
id of what came back : 135662073621888 <- the same object
__defaults__
is a tuple of the default values, and it is an attribute of the function like
any other. Nothing resets it between calls, because nothing was ever going to.
Once you see it as an attribute of the function object rather than an
instruction to be run, the behaviour stops being strange. def is a statement
that executes: it evaluates the defaults, builds a function object, and binds a
name to it.
It is not really about mutability
The mutable framing is how the problem is usually described, and it hides a second case:
def stamped(when=datetime.datetime.now()):
return when
two calls 50 ms apart return the same time : True
value : 2026-08-31 08:23:44.298407
Nothing was mutated. datetime.now() ran once, when the module was
imported, and every call since has returned that same moment. A long-running
process will happily report a timestamp from days ago.
The same applies to a default of uuid4(), time.time(), or anything else you
expected to be evaluated fresh.
Mutability only decides whether you notice:
defaults : ([], {}, set(), '', 0, ())
All six are shared. The str, int and tuple cannot be changed in place, so
sharing them has no visible effect. The list, dict and set can.
The fix
def collect_ok(item, into=None):
if into is None:
into = []
into.append(item)
return into
collect_ok('a') : ['a']
collect_ok('b') : ['b']
collect_ok.__defaults__ : (None,)
None is immutable, so sharing it costs nothing, and the [] is now built
inside the body — which does run on every call.
The same shape works for the timestamp:
def stamped_ok(when=None):
if when is None:
when = datetime.datetime.now()
with the None sentinel, same? False
When None is a legitimate value
Sometimes None is a meaningful argument and you need to tell “not passed”
from “passed as None”. Use a sentinel object of your own:
MISSING = object()
def setting(value=MISSING):
if value is MISSING:
return "no value passed"
return f"got {value!r}"
setting() : no value passed
setting(None) : got None
setting(0) : got 0
A bare object() is enough — it is unique, it compares equal to nothing else,
and is is the right test. This is the same pattern as the sentinel in the
article on checking whether a list is empty.
The same rule bites in class bodies
Where people meet this a second time, without recognising it:
class Basket:
items = [] # shared by every instance
def add(self, x):
self.items.append(x)
b1.items : ['apple']
b2.items : ['apple'] <- the same list
same object? True
A class body is executed once too, so items is one list belonging to the
class. self.items.append(...) finds it through the instance and mutates the
class attribute.
Assign it in __init__ instead:
class BasketOk:
def __init__(self):
self.items = []
with __init__ : c1=['apple'] c2=[]
A @dataclass handles this for you and will refuse a mutable default outright,
telling you to use field(default_factory=list).
It is not always a bug
The mechanism has legitimate uses, and it is worth knowing them so the rule does not become superstition.
A cache that lives on the function:
def fib(n, _cache={0: 0, 1: 1}):
if n not in _cache:
_cache[n] = fib(n - 1) + fib(n - 2)
return _cache[n]
fib(30) with a default-dict cache : 832040
cache size after : 31
It works, and I would still use
functools.lru_cache,
because it says what it means and does not put an implementation detail in the
signature.
The other one you will see in hot loops is binding a global to a local name:
def fast(n, _len=len):
return _len(n)
That turns a global lookup into a local one. It is a real optimisation and almost never worth the noise.
The leading underscore in both is the convention for “this parameter is not for you to pass”.
Letting a tool find it
Every linter knows about this:
- ruff and flake8-bugbear: B006, Do not use mutable data structures for argument defaults
- pylint: W0102,
dangerous-default-value
Both are on by default in the usual configurations. If you have a codebase of any age, it is worth running one just for this check.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).