FULL PYTHON FIX · 8 MIN READ
Why Does a Python Mutable Default Argument Keep Old Values?
A default list is created when the function is defined, so later calls can reuse it. Trace that shared object, then create a fresh list when the caller omits the argument.
Video guide: Why Does a Python Mutable Default Argument Keep Old Values?
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
I call add_tag once with red and once with blue. I do not pass a list in either call, so I might expect one fresh list for each call. Yet the second result contains red and blue. Let us run it before changing anything. The first output looks completely ordinary. The second shows that some state has survived across the calls. If this function is collecting labels for separate users, that is a real bug: the next user could inherit a previous user's labels. The interesting part is that append is doing exactly what append is supposed to do. The question is which list it is changing. In this video, we will inspect the function's default value, watch two results refer to one object, and then change the default to None.
- 00:47
We will also check the case where a caller intentionally provides a list, because a correct fix should have a clear contract for both kinds of call. The word True tells us more than an equality check would. First and second are names for the exact same list object. Python evaluates the default expression when the def statement runs. That empty list is stored with the function and reused when a call leaves out the tags argument. After the first call, the stored list has red. The second call appends blue to that same list. The inspection of __defaults__ makes the stored value visible, although ordinary application code should not reach into __defaults__ to reset it.
- 01:30
This behavior is specific to a mutable default whose contents we change. An integer default does not accumulate an increment in the same way, because adding one produces another integer rather than changing the integer object. The safe question to ask is: does the body mutate an object supplied as a default? If yes, repeated calls deserve a test. Calling the function once can never reveal this bug. The default is now None. When the caller leaves out tags, we create an empty list inside the call. That line runs each time it is needed, so the red call and the blue call get separate objects. The identity check is now False.
- 02:11
We used tags is None instead of if not tags for a reason. An empty list that a caller explicitly passes is false in a boolean test, but it is still a real list provided by the caller. If we used if not tags, we would silently replace that explicit list and fail to update it. None represents one special request: make a fresh list for me. An actual list, including an empty one, represents another request: append to this list. This is a small change to the code, but it makes ownership of the list much clearer. This call is deliberately different. Chosen is a list supplied by the caller, so the function appends to that exact list.
- 02:53
The result and chosen are the same object. This can be the behavior you want for a helper that updates an existing collection. If your function promises never to mutate caller input, then it needs another design: make a copy first and return the copy. Do not make that choice by accident. Notice that a simple list copy is shallow: if its elements are themselves lists or dictionaries, those inner objects are still shared. That is a separate contract question from the default argument bug, but it matters when you promise callers an unchanged input. A mutable default is not a syntax error, and there are rare cases where persistent state is intentional.
- 03:35
But if callers expect independent results, sharing the default is surprising and hard to spot at the call site. Testing the contract in both directions catches it: two omitted calls must be independent; one explicitly supplied list must behave as documented. On the website, write collect_label with a default of None. When labels is None, create a new list, append the requested label, and return it. Try two calls without a list and verify that each result contains only its own label. Then create an empty list outside the function, pass it in, and verify that the same list receives the label.
- 04:15
These tests catch both the original shared-state bug and the tempting but incorrect if not labels shortcut. As a second experiment, save the list returned by the first omitted call and print it after the second omitted call. In the broken version, that first result has changed to contain both labels, because it is the same object. In the repaired version, it stays unchanged. That observation can reveal this bug even when you only keep references to the returned results and never inspect the default itself. If you encounter a function that seems to remember data between calls, inspect its defaults and ask whether a list, dictionary, or set is being mutated there.
- 04:59
Reproduce the behavior with two calls in one process. Then choose explicitly whether the function should create fresh state or accept and modify caller state. Keep a small regression test with two independent calls, because this bug often returns during later refactoring. The full article has runnable examples and a checked practice task; the related functions and lists lessons explain the underlying tools.
Watch a second call inherit the first call's tag
Imagine a helper that collects one tag and returns the current tags. You expect each call without a supplied list to start empty. Run the small function twice and predict the second line before reading it. The first call looks fine. The second call includes the old tag as well as the new one, even though you did not pass a list to either call.
Nothing in the append method is broken. The problem is the list used as a default value. The function definition creates that list once, and calls that omit the argument use the same object. This can remain hidden when a test only calls the function once. Test repeated calls in the same Python process to reproduce the surprising behavior.
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag('red'))
print(add_tag('blue'))['red']
['red', 'blue']Inspect the one list held by the function
Python evaluates default argument expressions when the def statement executes, not each time a call begins. The function keeps those values in __defaults__. Inspect the first default after two calls: it contains both tags. You can also compare the two returned objects with is. It is True because both calls returned the same list, not merely two lists with equal contents.
The important distinction is mutation. An integer default such as count=0 does not accumulate changes when the body computes count + 1, because that expression makes another integer. A list can be changed in place with append, so the function's stored default itself changes. Resetting add_tag.__defaults__ manually would mask the symptom without giving callers a reliable contract.
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
first = add_tag('red')
second = add_tag('blue')
print(first is second)
print(add_tag.__defaults__[0])True
['red', 'blue']Use None to request a fresh list
Set the default to None, then create a new list inside the function only when no list was supplied. None is a useful sentinel here because it distinguishes an omitted argument from an actual list. Each call that omits tags now gets its own list. The returned lists hold red and blue separately, and the identity comparison is False.
Use the explicit comparison tags is None. Writing if not tags would also replace an intentionally supplied empty list, changing the caller's requested behavior. With the repair below, a caller that passes an existing list still asks the function to append to that list. If your function must never mutate a caller's list, copy it as a separate design choice and document that contract.
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
first = add_tag('red')
second = add_tag('blue')
print(first)
print(second)
print(first is second)['red']
['blue']
Falsedef add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
chosen = []
result = add_tag('green', chosen)
print(chosen)
print(result is chosen)['green']
TrueTest repeated calls and an intentional shared list
Run two calls without tags again. Neither should affect the other. Then pass a list explicitly and check both its contents and object identity. This verifies two different contracts: omitted lists are fresh, while an explicitly provided list is changed in place. Repeated calls matter more than a single happy-path output for this class of bug.
A mutable default is not forbidden in every program. It can deliberately act as persistent state, but that behavior should be explicit because callers otherwise cannot see the sharing at the call site. For ordinary collection helpers, the None sentinel is easier to reason about. Try an empty list supplied explicitly as well; tags is None preserves that exact list, whereas if not tags would silently replace it.
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
print(add_tag('one'))
print(add_tag('two'))
shared = ['start']
print(add_tag('three', shared))
print(shared)['one']
['two']
['start', 'three']
['start', 'three']- A default list is created once when the definition runs.
- None lets omitted arguments create fresh state inside the function.
- An explicitly supplied list is still the caller's object unless the function copies it.
Practice the fix
Write collect_label(label, labels=None). When labels is omitted, return a fresh list containing only that label. When the caller supplies a list, append to that same list and return it. Try two omitted calls and one call with an explicit empty list.
Need a hint?
Use if labels is None: labels = [] before labels.append(label). Avoid if not labels because an explicitly supplied empty list is valid.
Python console
Ready to runEdit the code and run it in your browser. Examples above can be loaded with Try this example.
Does your code use input()? Add one value per line
Your output appears here.
Write your solution, then select Check practice.
Key takeaways
- A mutable default value is evaluated once at function definition time.
- Calls that omit the argument can accidentally share and mutate that value.
- Use None as a sentinel and create a new list inside the function.
- Test repeated calls and explicitly supplied empty lists.