← Python Code Python's Hidden Traps
Browse Python Concepts

Mutable Default Arguments — The Bug That Accumulates State

Mental Model

Think of a mutable default argument as a sticky note attached to the function definition. Every call that uses the default reads from and writes to that same sticky note — it is never replaced with a fresh one.

Rule: Never use mutable objects like lists, dicts, or sets as default arguments; use None as a placeholder and instantiate them inside the function.

The Setup

You are building a lightweight request logging helper for an API gateway. The helper appends new log entries to a list. During local testing with single requests, it works perfectly, but under concurrent or sequential requests in staging, logs from different requests start mixing together.

What Does This Print?

Broken code
Python
def log_request(route, metadata=[]):
    metadata.append(route)
    return metadata

print(log_request("/v1/users"))
print(log_request("/v1/payments"))
Predict what the second print statement outputs. Does it return just the payment route, or does it include the users route?

The Output

What actually happens
['/v1/users'] ['/v1/users', '/v1/payments']

Instead of creating a fresh empty list on each call, the function reuses the exact same list instance that was instantiated when the module was first loaded. The second invocation appends to this existing list, corrupting isolated log paths.

Why Python Does This

In Python, functions are first-class objects. A function definition is an executable statement that runs once when the module is imported. During evaluation, Python computes the default argument expressions and stores them in the function object's __defaults__ attribute (a tuple). When the function executes, Python does not re-evaluate the default argument expression; instead, it binds the parameter name directly to the object stored in __defaults__. Since lists are mutable, modifying metadata modifies the underlying object inside __defaults__, persisting changes across all future invocations.

The Fix

Corrected pattern
Python
def log_request(route, metadata=None):
    # If no metadata is provided, default to a new list instance at execution time
    if metadata is None:
        metadata = []
    metadata.append(route)
    return metadata

Using None as the default and instantiating inside the function body means a brand-new list is created every time the function executes, because the function body re-runs on each call while the default argument expression does not. This ensures each call has its own isolated state.

How This Fails in Real Systems

An e-commerce startup used a mutable list default parameter to track applied discount codes. Over the course of a weekend, customers started noticing random discounts from other users' carts showing up in their final checkout screen. This data leak went unnoticed for 48 hours until high-volume logs revealed thousands of corrupted checkout sessions.

Key Takeaway

Never use mutable objects like lists, dicts, or sets as default arguments; use None as a placeholder and instantiate them inside the function.
Common mistake: Developers assume default argument values are re-evaluated on every function call, just like the function body is. They don't realize the default expression is evaluated only once when the function is defined.