Mutable Default Arguments — The Bug That Accumulates State
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.
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?
def log_request(route, metadata=[]):
metadata.append(route)
return metadata
print(log_request("/v1/users"))
print(log_request("/v1/payments"))
The Output
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
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.