Mutable default argument pitfall

RoadmapsPython Backend

Overview

Python evaluates default argument values once when the function is defined — not each time the function is called. If that default is a mutable object (list, dict, or set), all calls that omit the argument share the SAME object in memory. Example of the bug: def append_tag(tag, result=[]): result.append(tag) return result Calling append_tag('a') then append_tag('b') returns ['a', 'b'] on the second call — the list was mutated between calls and was never reset. The idiomatic fix is to use None as the default sentinel and create the mutable object inside the body: def append_tag(tag, result=None): if result is None: result = [] result.append(tag) return result Think of the default as a sticky note stapled to the function when Python first reads its definition — every call reads the same note, not a fresh copy printed for each call.

This bug is invisible at a glance: the signature looks correct and the very first call always works. The symptom is mysterious state persistence across calls — data from one request leaking into the next in API handlers, or factory functions producing objects that share hidden state.

Where used: FastAPI dependency factories, Pydantic model_validator helpers, argument accumulator patterns in CLI tools

Why learn this

Code walkthrough

def add_field(key, store={}):
    store[key] = True
    return store

print(add_field('name'))
print(add_field('email'))
print(add_field('age'))

Focus: store is created once at function definition; each call without an explicit argument mutates the same dict, so the printed output grows with every call — the bug in action.

Common mistakes

Glossary

sentinel
A special, unique value (like None) used to indicate that no data was provided or to signal the end of a process, e.g. `def process(items=None): if items is None: items = []`.
cross-call
Events or data that carry over from one execution of a function to the next time that same function is called, e.g. a shared mutable default list that retains items between calls.

Recall questions

Understanding checks

A developer expects this function to print the current time each time it's called. What actually happens?

The time is recorded once when the function is defined. Every call that omits `t` will print the exact same timestamp.

In Python, default arguments are evaluated exactly once at function definition time, not each time the function is called. Thus, `time.time()` executes only once.

What is the output of this code?

2 3 8

The first call `multiply(2)` appends 2 to the default list and returns 2. The second call `multiply(3, [])` passes a new list, so it returns 3 without affecting the default list. The third call `multiply(4)` uses the default list again, which still holds `[2]`. It appends 4, so the list becomes `[2, 4]`, and it returns `2 * 4 = 8`.

Practice tasks

Fix a leaking tag accumulator

The function below accumulates tags across calls because of a mutable default argument. Modify the function to fix this bug so that each call without a `tags` argument starts fresh. Output should be `['admin']`, `['user']`, `['guest']`.

Challenge

Request header builder

Write add_header(key, value, headers=None) that adds key:value to a headers dict and returns it. Demonstrate that two independent call chains each produce their own dict — no shared state between them. Print both dicts at the end.

Continue learning

Previous: Mutable vs immutable types

Next: Shallow vs deep copy

Return to Python Backend Roadmap