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:
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())
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
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=):
results :
results = []
results.append(node[])
child node.get(, []):
walk(child, results)
results
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):
data :
data = {}
data:
ValueError()
data
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
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 |
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
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.
Catch Multiple Exceptions in One except Block in Python
Learn how to catch multiple exceptions in one Python except block using tuples, with examples, mistakes, and real-world usage.
Convert Bytes to String in Python 3
Learn how to convert bytes to str in Python 3 using decode(), text mode, and proper encodings with practical examples.