datarekha
Python Easy Asked at GoogleAsked at AmazonAsked at Meta

What is the mutable default argument trap in Python, and how do you fix it?

The short answer

Default argument values are evaluated once when the function is defined, not each time it is called. If the default is a mutable object like a list or dict, all calls that use the default share the same object — so mutations in one call persist into the next. The fix is to use None as the default and create the mutable object inside the function body.

How to think about it

This is one of Python’s most dependable interview questions, precisely because the bug stays invisible until you know where to look. What it really tests is whether you understand when a default value is born — at definition time, not at each call.

Here is the fact to hold onto: def is itself an executable statement. When Python runs it, it evaluates every default expression once and tucks the results into function.__defaults__. So the [] you typed in the signature is not a fresh-each-time template — it is one particular list object that lives for as long as the function does. Every call that falls back on the default reaches for that same list.

Watch the bug accumulate

# BUGGY — the default list is created once and quietly reused
def add_item_bug(item, container=[]):
    container.append(item)
    return container

print(add_item_bug("a"))
print(add_item_bug("b"))     # not ['b'] — the list remembers
print(add_item_bug("c"))
print("Stored default:", add_item_bug.__defaults__)

# FIXED — None is the sentinel; build a fresh list inside the body
def add_item(item, container=None):
    if container is None:
        container = []
    container.append(item)
    return container

print(add_item("a"))
print(add_item("b"))
print(add_item("c"))
['a']
['a', 'b']
['a', 'b', 'c']
Stored default: (['a', 'b', 'c'],)
['a']
['b']
['c']

The buggy function’s three calls share one list, so it grows to a, ab, abc — and you can watch the accumulated state hang off __defaults__. The fixed function mints a new list on every call that omits the argument, so each result stands alone.

Why None is the right sentinel

None is immutable, so it can never quietly accumulate anything. The guard if container is None: container = [] then builds a fresh list each time the caller leaves the argument out — while a caller who does pass a list still gets exactly the object they passed. The fix costs two lines and breaks nothing.

The same trap, other containers

It isn’t only lists — any mutable default behaves this way:

# Broken — one dict shared across calls
def register(name, registry={}):
    registry[name] = True
    return registry

# Fixed
def register(name, registry=None):
    if registry is None:
        registry = {}
    registry[name] = True
    return registry

When you actually want it

Once in a while the shared default is the point — a hand-rolled cache exploits it deliberately:

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 functools.lru_cache says the same thing more clearly and is what you’d actually reach for.

Learn it properly Variables & Types

Keep practising

All Python questions

Explore further

Skip to content