Skip to content
SDETRole
SDETRole

Python interview question

What is the mutable default argument pitfall in Python?

beginner 0–2 yrs #functions#default-arguments#mutability#pitfalls

Short answer

Default values are evaluated once, when the def statement runs, not on each call. A mutable default such as a list or dict is therefore shared across calls and accumulates state. Use None as the default and create the object inside the function. The same applies to defaults like datetime.now().

Answer

In Python, default argument values are evaluated once, when the def statement is executed (usually at import time), and the same object is reused for every call that does not pass that argument. If the default is mutable (a list, dict, set or custom object) and the function modifies it, the changes persist into later calls.

The bug

def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags

print(add_tag("smoke"))   # ['smoke']
print(add_tag("ui"))      # ['smoke', 'ui']   <- surprise
print(add_tag.__defaults__)  # (['smoke', 'ui'],)

The list lives on the function object (__defaults__), so every call that omits tags mutates the same list.

The fix: None as a sentinel

def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
    if tags is None:
        tags = []
    tags.append(tag)
    return tags

Use is None rather than if not tags, because an empty list passed deliberately by the caller is falsy and would be replaced.

If None is a valid value for the argument, use a private sentinel object:

_MISSING = object()

def configure(timeout=_MISSING):
    if timeout is _MISSING:
        timeout = load_default_timeout()
    ...

Why it matters in test code

This bug is common in test data builders and helpers, and it causes order-dependent failures that are hard to diagnose:

def build_order(items=[], status="pending"):   # shared list!
    items.append({"sku": "TSHIRT", "qty": 1})
    return {"items": items, "status": status}

def test_one_item():
    assert len(build_order()["items"]) == 1    # passes when run first

def test_order_status():
    order = build_order()
    assert len(order["items"]) == 1           # fails: 2 items, depends on test order

Run alone, each test passes; run together, the second fails. With pytest-xdist or random ordering (pytest-randomly), it becomes intermittent.

Not only mutables: evaluated-once values

Any expression in a default runs once:

from datetime import datetime, UTC

def log(msg, at=datetime.now(UTC)):   # timestamp frozen at import time
    print(at, msg)

The same applies to uuid.uuid4() or a randomly generated email in a default. Use None and compute inside the function.

Dataclasses guard against it

from dataclasses import dataclass, field

@dataclass
class TestUser:
    roles: list[str] = field(default_factory=list)

A dataclass raises ValueError if you write roles: list[str] = [], which is a helpful guard. Pydantic models copy mutable defaults per instance, so the pitfall does not apply there in the same way.

Tooling

Linters catch this: Ruff and flake8-bugbear rule B006 (mutable default) and B008 (function call in default), and Pylint W0102 (dangerous-default-value).

What interviewers look for

  • The precise cause: evaluated once at definition time.
  • The None sentinel fix with is None.
  • Awareness of how it causes order-dependent test failures.
  • Bonus: default_factory, timestamps in defaults, lint rules.

A strong answer covers

  • Defaults are evaluated once at function definition time
  • A mutable default is shared between calls
  • Fix: use None as a sentinel and create inside
  • Explains mutable vs immutable types
  • Mentions other evaluated-once defaults like timestamps or linters that catch it
Practise answering under time pressure

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →