Skip to main content
Articles/python-pitfalls/python-s-tricky-default-arguments
Scenario Article

Python's Tricky Default Arguments

Why It Happens

Abstract Algorithmsβ€’Jul 13, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

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

1

Stage 1 β€” The Unexpected Behavior

Action / Decision:

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?

Component Behavior:

Many new Pythonistas expect [4] , but the actual output reveals the shared state:

Observable Output: Many new Pythonistas expect [4] , but the actual output reveals the shared state:
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

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.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit