AcademyPython
Mutable Default Arguments
Why a default value written as a list or dict is created once, shared across every call, and the None-then-create idiom that every codebase uses to avoid it.
3 min readUpdated 3 exercises
The bug in five lines
def add_item(item, basket=[]):
basket.append(item)
return basket
add_item('apple') # ['apple']
add_item('banana') # ['apple', 'banana'] — not ['banana']The second call was supposed to start from an empty basket, since no basket argument was passed either time. Instead it kept appending to the same list. This is not a rare edge case — it is the single most-cited gotcha in Python, and it follows directly from a rule most people learn about defaults only after being bitten by it once.
Why it happens
A def statement runs exactly once, when the module is loaded — not once per call. Everything in the signature, including basket=[], is evaluated at that moment, and the resulting list object is stored on the function itself as its default. Every call that omits basket receives that same object, not a fresh copy of it:
add_item.__defaults__ # ([...],) — one list, reused foreverA default value that is an immutable type — a number, a string, None, a tuple of immutables — cannot expose this, because there is no way to mutate it in place; any operation on it produces a new object instead of changing the shared one. The bug is specific to mutable defaults: lists, dicts, sets, and any custom object with methods that mutate it.
The fix
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basketNow the default stored on the function is None — immutable, harmless to share — and a fresh list is created inside the body on every call that needs one. This is not a workaround; it is the idiom. Seeing =None followed by an if x is None: x = ... line at the top of a function is the normal, expected shape of a Python function that wants a mutable default, and its absence in a code review is usually worth a comment.
The same rule bites class attributes too
class Basket:
items = [] # class attribute — one list shared by every instance
def add(self, item):
self.items.append(item)
a = Basket()
b = Basket()
a.add('apple')
b.items # ['apple'] — b never called add, but shares a's listitems = [] at class body level is evaluated once, exactly like a default argument, and every instance that has not set its own items reads the same list off the class. The fix has the same shape as the function version — assign the mutable value inside __init__ instead, where it runs once per instance:
class Basket:
def __init__(self):
self.items = []Why this rule surprises people used to other languages
Most languages that support default parameter values re-evaluate the default expression on every call — items=[] in one of those languages really does mean "a new empty list, every time." Python evaluates default expressions once, at definition time, for every type of default without exception. The behaviour is entirely consistent — a number default and a list default follow the identical rule — but only the mutable case has any way to reveal it, which is why the bug reads as list-specific when it is really a definition-time-evaluation rule applied uniformly.
Spotting it without running the code
The signal is a default value written with brackets, braces, or a constructor call that returns a mutable object — =[], ={}, =set(), =SomeClass() — anywhere in a function or method signature. None of those are safe as a literal default the moment the function is expected to be called more than once with that default in play.
Now practice it
1 challenge · 2 quizzesReading this page is the cheap half. These are the exercises that make you use it.
- Fundamentals · Advanced PatternsAdvanced-pattern questions that cover this exact class of definition-time-evaluation gotcha.QuizTake · HardQuizTake · Hard
- Object-Oriented Programming · Advanced OOPExtends the same rule to shared class attributes, the other place it shows up.QuizTake · HardQuizTake · Hard
- Closures — Make MultiplierProA closer look at what a function remembers between calls, the same mechanism this bug exploits by accident.ChallengeSolve · EasyChallengeSolve · Easy
Read next
Args and kwargs in Python
How the star and double-star collectors turn extra positional and keyword arguments into a tuple and a dict, and where positional-only and keyword-only parameters fit.
3 min
OOP and Dunder Methods in Python
How __init__, __repr__ and __eq__ hook a class into built-in operators, why super() matters in a diamond hierarchy, and when composition beats inheritance.
3 min