Question
Python Mutable Default Arguments Explained: Why Defaults Are Evaluated Once
Question
In Python, why is a mutable default argument shared across function calls instead of being recreated each time the function runs?
Consider this example:
def foo(a=[]):
a.append(5)
return a
A beginner might expect every call without an argument to return a new list containing only one value:
print(foo()) # [5]
print(foo()) # expected: [5]
print(foo()) # expected: [5]
But Python actually behaves like this:
print(foo())
print(foo())
print(foo())
print(foo())
Output:
[5]
[5, 5]
[5, 5, 5]
[5, 5, 5, 5]
A related example makes the behavior even clearer:
def a():
print("a executed")
return []
def b(x=a()):
x.append(5)
print(x)
This prints a executed only once, when the function is defined, not each time it is called:
b() # [5]
b() # [5, 5]
Why does Python evaluate default arguments at function definition time rather than at call time? What is the design reason behind this behavior, and what is the correct way to avoid bugs caused by mutable defaults?
Short Answer
By the end of this page, you will understand why Python evaluates default arguments once when a function is defined, why mutable defaults like lists and dictionaries can be unexpectedly shared across calls, and how to write safe functions using None or other sentinel values.
Concept
In Python, default argument values are evaluated exactly once, at the moment the def statement runs. They are then stored inside the function object and reused on later calls.
That means this function:
def foo(a=[]):
a.append(5)
return a
creates one list object when Python defines foo. Every call to foo() without an explicit argument uses that same list.
This is not specific to lists. It applies to all default values, but it becomes surprising when the default is mutable:
listdictset- most custom objects
If the default value is immutable, such as None, 0, False, or a string, the effect is usually harmless because immutable objects cannot be changed in place.
Why Python does this
Python treats the def statement as executable code. When Python reaches a function definition:
- It evaluates the default expressions.
- It creates the function object.
- It stores those defaults as part of that function object.
This keeps function definition behavior consistent and predictable. The default is part of the function's definition, not something recomputed later unless you explicitly code it that way.
Why this matters
This matters because beginners often assume defaults behave like "fresh values" created for each call. In Python, they are stored values, not templates.
That can cause bugs such as:
- data leaking between calls
- state being accidentally remembered
- confusing test failures
- functions behaving differently depending on how many times they were already called
Is it ever useful?
Yes, sometimes. A mutable default can intentionally be used to preserve state across calls, similar to cached state or a static variable. But this is uncommon and usually avoided because it is easy to misunderstand.
Most of the time, the correct approach is to use None and create a new object inside the function.
Mental Model
Think of a Python function definition like packing a toolbox once.
When Python reads this:
def foo(a=[]):
...
it puts one specific list into the toolbox for foo. Later, every time you call foo() without giving a, Python pulls out that same list from the toolbox.
It does not build a new list each time.
So this is not:
- "use
[]as instructions to make a new list later"
It is:
- "store this already-created list as the default"
A good analogy:
- Definition time = writing and packing the function
- Call time = using the packed function
- Mutable default = reusing the same notebook every time instead of handing out a fresh blank page
If you write in that notebook during one call, the next call sees the previous notes.
Syntax and Examples
The unsafe pattern looks like this:
def add_item(item, items=[]):
items.append(item)
return items
Example:
print(add_item("a"))
print(add_item("b"))
Output:
['a']
['a', 'b']
The second call reuses the same list.
Safe pattern: use None
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Now each call without items gets a fresh list:
print(add_item("a"))
print(add_item("b"))
Output:
['a']
['b']
Passing your own list still works
my_list = [1, 2]
print(add_item(3, my_list))
print(my_list)
Output:
[1, 2, 3]
[1, 2, 3]
This is expected because you explicitly passed in a mutable object, so the function modifies that object.
Definition-time evaluation example
def make_list():
print("creating list")
return []
def demo(x=make_list()):
x.append(1)
return x
When Python defines demo, it prints:
creating list
Then:
print(demo())
print(demo())
Output:
[1]
[1, 1]
So the default expression ran once, not once per call.
Step by Step Execution
Trace this example:
def foo(a=[]):
a.append(5)
return a
print(foo())
print(foo())
Step 1: Python reads the def
def foo(a=[]):
At this moment:
- Python creates a new empty list
[] - Python creates the function object
foo - Python stores that list as the default value for parameter
a
Step 2: First call
foo()
Because no argument is passed:
- Python uses the stored default list
arefers to that lista.append(5)changes the list from[]to[5]- function returns
[5]
Step 3: Second call
foo()
Again, no argument is passed:
- Python uses the same stored default list
- that list is already
[5] a.append(5)changes it to[5, 5]- function returns
[5, 5]
Visual view
After definition:
foo defaults -> []
After first call:
foo defaults -> [5]
After second call:
foo defaults -> [5, 5]
Safe version trace
def foo(a=None):
if a is None:
a = []
a.append(5)
return a
Now on each foo() call:
astarts asNone- the function creates a fresh list inside the function body
- each call gets a separate list
Real World Use Cases
Mutable default arguments usually appear as a bug, but understanding them helps in real code.
1. Building request payloads
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
Used when assembling API request data without leaking tags between requests.
2. Collecting validation errors
def validate_user(user, errors=None):
if errors is None:
errors = []
if not user.get("name"):
errors.append("name is required")
return errors
If errors=[] were used directly, one user's errors could carry into another validation run.
3. Recursive tree traversal
Beginners often write recursive functions with a list default.
def walk(node, results=None):
if results is None:
results = []
results.append(node["name"])
for child in node.get("children", []):
walk(child, results)
return results
This is safe because the list is created once per top-level call, not once for the entire lifetime of the function.
4. Intentional persistent state
Sometimes developers intentionally reuse a default object:
def counter(state={"count": 0}):
state["count"] += 1
return state["count"]
This works, but it is usually avoided because it hides state in a surprising place. A class or closure is clearer.
Real Codebase Usage
In real projects, developers almost always avoid mutable defaults in public functions.
Common safe pattern
def process(items=None):
if items is None:
items = []
# work with items
This is widely used in:
- web backends
- CLI tools
- data processing scripts
- libraries
Guard clause style
def save_records(records=None):
if records is None:
records = []
if not records:
return 0
return len(records)
Developers often combine None defaults with early returns.
Validation pattern
def create_user(data=None):
if data is None:
data = {}
if "email" not in data:
raise ValueError("email is required")
return data
This prevents accidental data sharing between calls.
Configuration objects
Instead of this:
def connect(options={}):
options["timeout"] = 5
return options
developers write:
def connect(options=None):
if options is None:
options = {}
options.setdefault("timeout", 5)
return options
If persistent state is needed
Real code usually prefers one of these instead:
- a class instance
- a closure
functools.lru_cache- a module-level variable
These approaches make shared state explicit and easier to maintain.
Common Mistakes
Mistake 1: Using a list or dict as a default
Broken code:
def add_name(name, names=[]):
names.append(name)
return names
Problem:
- values accumulate across calls
Fix:
def add_name(name, names=None):
if names is None:
names = []
names.append(name)
return names
Mistake 2: Checking with if not items instead of if items is None
Broken code:
def add_item(item, items=None):
if not items:
items = []
items.append(item)
return items
Problem:
- if the caller passes an empty list
[], it is treated the same asNone - that may replace a real argument unexpectedly
Better:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Mistake 3: Forgetting that passed-in objects are still mutable
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
my_items = []
add_item("x", my_items)
print(my_items)
Output:
['x']
This is not a default-argument bug. You passed a list explicitly, so the function mutated it.
Mistake 4: Assuming default expressions run each call
Broken assumption:
def now_value(x=get_time()):
return x
If get_time() returns the current time, that time is captured once when the function is defined.
Fix:
def now_value(x=None):
if x is None:
x = get_time()
return x
Mistake 5: Using mutable defaults to hide state
def next_id(state={"id": 0}):
state["id"] += 1
return state["id"]
This may work, but it is confusing. Prefer a class or closure so the state is obvious.
Comparisons
| Concept | When evaluated | Shared across calls? | Safe for mutable data? | Typical use |
|---|---|---|---|---|
def f(x=[]) | At function definition | Yes | No | Usually a bug |
def f(x=None) then x = [] inside | At call time for the new list | No | Yes | Recommended pattern |
Immutable default like def f(x=0) | At function definition | Yes, but harmless | Usually yes | Very common |
| Explicit argument passed by caller | At call time by caller | Depends on passed object | Depends | Normal function usage |
Mutable vs immutable defaults
| Default value | Mutable? | Risky as default? |
|---|---|---|
[] | Yes | Yes |
{} | Yes | Yes |
set() | Yes | Yes |
None | No | No |
0 | No | No |
"text" | No | No |
(1, 2) | No | No |
Hidden state vs explicit state
| Approach | Readability | Good for shared state? |
|---|---|---|
| Mutable default argument | Low | Rarely |
| Class attribute or instance attribute | High | Yes |
| Closure variable | Medium | Yes |
| Module-level variable | Medium | Sometimes |
Cheat Sheet
Rule
Default argument expressions in Python are evaluated once, when the function is defined.
Risky pattern
def func(items=[]):
items.append(1)
return items
Safe pattern
def func(items=None):
if items is None:
items = []
items.append(1)
return items
Use is None, not if not items
if items is None:
items = []
Affects all mutable objects
listdictset- custom mutable instances
Usually safe defaults
None- numbers
- strings
- tuples
- booleans
Key idea
def f(x=some_expression()):
some_expression() runs when def executes, not every time f() is called.
If you need shared state
Prefer:
- a class
- a closure
- a cache
- a module variable
instead of a mutable default.
FAQ
Why does Python evaluate default arguments only once?
Because the default value is part of the function object created when the def statement runs.
Are mutable default arguments always bad?
Not always, but they are usually confusing and error-prone. Most code should avoid them.
Why is None the recommended default?
None is immutable and commonly used as a sentinel meaning "no value was provided." That lets you safely create a new list or dict inside the function.
Does this behavior apply to dictionaries too?
Yes. Any mutable object used as a default can be shared across calls.
Is this behavior a bug in Python?
No. It is intentional and consistent with how function definitions are executed.
Can this behavior ever be useful?
Yes, for intentionally preserving state across calls, but clearer alternatives usually exist.
How can I inspect a function's defaults?
You can look at function.__defaults__.
def foo(a=[]):
pass
print(foo.__defaults__)
Do lambda functions behave the same way?
Yes. Default arguments in lambdas follow the same rule because they are functions too.
Mini Project
Description
Build a small task collector function that safely adds tasks to a list. This project demonstrates the correct way to handle optional list arguments without accidentally sharing data between function calls.
Goal
Create a function that adds a task to a list and returns the updated list, while ensuring each call gets a fresh list unless one is explicitly provided.
Requirements
- Write a function named
add_task. - The function must accept a task name and an optional list of tasks.
- If no list is provided, create a new empty list.
- Append the new task and return the updated list.
- Show that separate calls without a list do not share data.
- Show that passing an existing list updates that specific list.
Keep learning
Related questions
@staticmethod vs @classmethod in Python Explained
Learn the difference between @staticmethod and @classmethod in Python with clear examples, use cases, mistakes, and a mini project.
Add Rows to a Pandas DataFrame in Python
Learn how to add rows to a Pandas DataFrame, why repeated row appends are slow, and when to use loc, concat, or record lists.
Call a Function by Name in a Python Module
Learn how to call a function by name in a Python module using strings, getattr, and safe patterns for dynamic function dispatch.