What's the mutable default argument trap, and how do mutable vs immutable types cause it?

5 minintermediatefundamentalsmutabilitygotcha

Quick Answer

Default argument values are evaluated **once**, when the `def` statement runs, not on every call. If the default is a mutable object (a list, dict, or set), every call that doesn't pass that argument **shares and mutates the same object**, causing state to leak across calls. Fix it by defaulting to `None` and creating the mutable object inside the function body.

Detailed Answer

The trap

def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

add_item("a")   # ['a']
add_item("b")   # ['a', 'b']  -- surprise! same list as before

The default [] is created once, at function-definition time, and stored on the function object (add_item.__defaults__). Every call that omits bucket reuses that exact same list, so mutations accumulate across unrelated calls.

Why immutable defaults don't have this problem

def greet(name, suffix="!"):
    return name + suffix

"!" is immutable — nothing inside greet can mutate the string object itself, so there's no shared, mutable state to leak. The bug is specific to mutable default values (list, dict, set, or any mutable custom object).

The fix

def add_item(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

Now a fresh list is created on every call that doesn't supply bucket, while callers who do want to accumulate into a shared list can still pass one explicitly.

The general lesson: mutable vs immutable

  • Immutable (int, float, str, tuple, frozenset, bytes): any "modification" creates a new object; the original is never changed. Safe to share across function calls, default arguments, and dict keys.
  • Mutable (list, dict, set, most custom classes): the object can be changed in place; sharing a reference means all holders see the mutation. Never use a mutable object as a default argument, and be careful when a mutable object is a class attribute (shared across all instances) instead of an instance attribute (set in __init__).
class Bad:
    items = []          # class attribute — shared by every instance!
    def __init__(self):
        pass

class Good:
    def __init__(self):
        self.items = []  # instance attribute — one per object

Interview-ready summary: Default arguments are evaluated once at def-time and stored on the function object, so a mutable default is shared across every call that uses it. Always default mutable arguments to None and construct the real object inside the function body — and apply the same caution to mutable class attributes.