Python’s Most Misunderstood Feature: Mutable Defaults

Understanding mutable default arguments isn’t just for writing cleaner code — it’s key to avoiding hidden bugs and mysterious behavior in…

Python’s Most Misunderstood Feature: Mutable Defaults
Photo by Glen Carrie on Unsplash

Understanding mutable default arguments isn’t just for writing cleaner code — it’s key to avoiding hidden bugs and mysterious behavior in your Python functions.

If you’ve ever found yourself baffled by why a Python function is returning unexpected results — especially when default arguments are involved — you’re not alone.

There’s one quirky feature in Python that trips up even experienced developers: mutable default arguments.

It’s subtle, counterintuitive, and can lead to bugs that are notoriously hard to track down.

Let’s dive into why this happens, what it really means, and most importantly, how you can avoid it.


The Unexpected Behavior

Consider this simple Python function:

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

Now try calling it a few times:

print(add_item('apple'))     # ['apple'] 
print(add_item('banana'))    # ['apple', 'banana'] 
print(add_item('cherry'))    # ['apple', 'banana', 'cherry']

Wait… what?

Shouldn’t the function return a new list each time?

That’s the common assumption — and it’s wrong.

What’s Going On Under the Hood?

The confusion stems from how default arguments are evaluated in Python.

In Python, default parameter values are evaluated only once — at the time the function is defined, not each time it’s called.

So in our example:

def add_item(item, item_list=[]):

The item_list=[] part creates a list once, and that same list is reused on every call where item_list isn’t explicitly provided.

Why This Is Dangerous

The reason this behavior is so misleading is because most developers expect a new list to be created each time the function is called — as it would be in many other programming languages.

Instead, the default list keeps growing with each call. If you’re writing functions that are meant to be stateless or pure, this can introduce bugs that silently compound over time.

It’s not limited to lists either. Any mutable object — like dictionaries or sets — will behave the same way.

The Correct Way: Use None as a Sentinel

The fix is surprisingly simple and Pythonic:

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

Now the output is as expected:

print(add_item('apple'))     # ['apple'] 
print(add_item('banana'))    # ['banana'] 
print(add_item('cherry'))    # ['cherry']

Each call creates a fresh list, because the mutable default is now initialized inside the function body.

Rule of Thumb

Never use mutable objects as default arguments.

Stick to immutable defaults like:

  • None
  • 0
  • '' (empty string)
  • () (empty tuple)

And initialize mutable structures — like lists or dicts — inside the function when needed.

Real-World Example: Caching Gone Wrong

Imagine a function intended to cache results:

def memoize(n, cache={}): 
    if n in cache: 
        return cache[n] 
    # Simulate computation 
    result = n * 2 
    cache[n] = result 
    return result

It works… but now cache is shared across all calls! This can lead to incorrect data reuse and memory leaks if not handled carefully.

A better approach?

Use None and initialize a local dictionary, or use decorators with care (like functools.lru_cache for real memoization).

A Deeper Truth: It’s Not a Bug, It’s a Feature

Python isn’t broken — it’s just explicitly designed this way.

This behavior allows default values to be anything — including precomputed values, functions, even open file handles — without recreating them every call. It’s powerful, but it comes with sharp edges.

That’s why understanding this design choice is essential to mastering Python.


Final Thoughts

Mutable default arguments are a classic Python pitfall, and one that’s easy to fall into — especially when you’re learning the language or writing quick functions.

But once you get the hang of it, this “gotcha” becomes a tool in your toolbox. You’ll write safer, cleaner, and more predictable code — and be the person who explains this to the next confused developer 😄

Bonus Tip:

Use linters like flake8 or pylint — they often warn you when you’re using mutable default arguments!


Was this helpful? Follow me for more Python deep dives and coding insights you won’t find in the docs.