Question

Python Mutable Default Arguments Explained: Why Defaults Are Evaluated Once

pythonlanguage-designdefault-parametersleast-astonishment

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:

  • list
  • dict
  • set
  • 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:

  1. It evaluates the default expressions.
  2. It creates the function object.
  3. 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.

Take Quiz

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
  • a refers to that list
  • a.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:

  • a starts as None
  • 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.

Take Quiz

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 as None
  • 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

ConceptWhen evaluatedShared across calls?Safe for mutable data?Typical use
def f(x=[])At function definitionYesNoUsually a bug
def f(x=None) then x = [] insideAt call time for the new listNoYesRecommended pattern
Immutable default like def f(x=0)At function definitionYes, but harmlessUsually yesVery common
Explicit argument passed by callerAt call time by callerDepends on passed objectDependsNormal function usage

Mutable vs immutable defaults

Default valueMutable?Risky as default?
[]YesYes
{}YesYes
set()YesYes
NoneNoNo
0NoNo
"text"NoNo
(1, 2)NoNo

Hidden state vs explicit state

ApproachReadabilityGood for shared state?
Mutable default argumentLowRarely
Class attribute or instance attributeHighYes
Closure variableMediumYes
Module-level variableMediumSometimes
Take Quiz

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

  • list
  • dict
  • set
  • 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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more