Python's Tricky Default Arguments
Why It Happens
Python's default arguments are evaluated once when the function is defined, not on each call.
π― System Scenario & Requirements
System Goal
Python's default arguments are evaluated once when the function is defined, not on each call.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: Python's default arguments are evaluated once when the function is defined, not on each call.
- If a default argument is a mutable object (like a list or dictionary π), all subsequent calls that don't override that argument will share the same mutable object, leading to surprising side effects and difficult-to-deβ¦
- Have you ever encountered a Python function where a default list or dictionary parameter seemed to retain changes from previous calls, even though you expected it to be fresh each time?
Constraints & Assumptions
- Constraints are derived from the rebuilt scenario stages.
β‘ Execution Stages & Decision Trace
Stage 1 β The Unexpected Behavior
Consider a function designed to add an item to a list, with an empty list as its default: What would you expect the output of the last line to be?
Many new Pythonistas expect [4] , but the actual output reveals the shared state:
Deep Dive
Scenario context
Python's default arguments are evaluated once when the function is defined, not on each call. If a default argument is a mutable object (like a list or dictionary π), all subsequent calls that don't override that argument will share the same mutable object, leading to surprising side effects and difficult-to-debug bugs π.
Have you ever encountered a Python function where a default list or dictionary parameterβ¦
This walkthrough focuses on Why It Happens.
Before making a design choice, clarify:
- The core issue is that default arguments are evaluated only once when the function object is created (i.e., when the def statement is executed).
- For immutable types (numbers, strings, tuples, None ), this isn't a problem because operations on them create new objects.
- However, for mutable types like lists, dictionaries, or sets, the same object instance is reused across all calls where the default is not explicitly overridden.
Concepts in play: python-concurrency-nogil, caching
Stage 1 β The Unexpected Behavior
- Consider a function designed to add an item to a list, with an empty list as its default: What would you expect the output of the last line to be?
- Many new Pythonistas expect [4] , but the actual output reveals the shared state:
def add_to_list(item, my_list=[]):
my_list.append(item)
return my_list
print(add_to_list(1))
print(add_to_list(2))
print(add_to_list(3, ['a', 'b']))
print(add_to_list(4))
[1]
[1, 2]
['a', 'b', 3]
[1, 2, 4] # Wait, what?!
Observable outcome: Many new Pythonistas expect [4] , but the actual output reveals the shared state:
Stage 2 β The Fix: Using None
- The standard and recommended solution is to use None as the default value, and then check inside the function body to initialize a new mutable object if None was passed.
- This ensures a fresh mutable object is created for each call that relies on the default.
- Now, the output is as expected:
def add_to_list_fixed(item, my_list=None):
if my_list is None:
my_list = [] # A new list is created only if no list was provided
my_list.append(item)
return my_list
print(add_to_list_fixed(1))
print(add_to_list_fixed(2))
print(add_to_list_fixed(3, ['a', 'b']))
print(add_to_list_fixed(4))
[1]
[2]
['a', 'b', 3]
[4]
Observable outcome: Now, the output is as expected:
End-to-end outcome
After working through the stages, you should be able to explain:
- Always be wary of using mutable objects (lists, dictionaries, sets, custom class instances) as default arguments in Python.
- The None idiom is your reliable pattern for ensuring that default mutable arguments behave as independent instances for each function call, preventing unexpected shared state and making your code more predictable and roβ¦
Alternatives considered
The source focuses on one primary design. A second architecture is not compared, so no alternative is invented here.
Failure and degraded modes
The source does not isolate a separate failure mode. Limits that matter for the scenario are called out in the stage decisions above.
Key learnings
- Always be wary of using mutable objects (lists, dictionaries, sets, custom class instances) as default arguments in Python.
- The None idiom is your reliable pattern for ensuring that default mutable arguments behave as independent instances for each function call, preventing unexpected shared state and making your code more predictable and roβ¦
- Consider a function designed to add an item to a list, with an empty list as its default: What would you expect the output of the last line to be?
- The standard and recommended solution is to use None as the default value, and then check inside the function body to initialize a new mutable object if None was passed.
