Course Content
Python Essentials for AI Engineer
6 sections · 48 lessons
What is the difference between a mutable default argument and a safe default argument?
What you need to know
The bug
1def add_item(item, bucket=[]): # unsafe: one list for ALL calls2 bucket.append(item)3 return bucket45print(add_item(1)) # [1]6print(add_item(2)) # [1, 2] <- leaked from the first call7print(add_item.__defaults__) # ([1, 2],)__defaults__ shows where the list lives: on the function object itself, created once when def ran. Every call that does not pass bucket gets that same list.
The fix
1def add_item(item, bucket=None):2 if bucket is None: # a new list on every call3 bucket = []4 bucket.append(item)5 return bucket67print(add_item(1), add_item(2)) # [1] [2]Use if bucket is None, not bucket = bucket or []. The or version also replaces an empty list the caller passed in, so the caller's list never receives the item — a quieter bug.
Why Python works this way
Defaults are part of the function definition, just as the function body is. Evaluating them once is simple and fast, and it is sometimes used on purpose as a cache. The language will not change it, so linters (Ruff rule B006, Pylint W0102) flag mutable defaults instead.
Dataclasses protect you
1from dataclasses import dataclass, field23@dataclass4class Conversation:5 user: str6 history: list[str] = field(default_factory=list) # new list per instance78a, b = Conversation("asha"), Conversation("ravi")9a.history.append("hi")10print(b.history) # []Writing history: list[str] = [] in a dataclass raises a ValueError at class definition time, exactly to stop this bug.
A real-life example
A chatbot service keeps conversation history like this:
1def chat(user_msg, history=[]):2 history.append({"role": "user", "content": user_msg})3 return len(history)45print(chat("Hi, I'm Asha. My order 1142 is late.")) # 16print(chat("Hi, I need a refund.")) # 2 <- a different user!In a web server the function object lives for the life of the process, so every user who does not pass history shares one list. The second user's prompt now contains the first user's name and order number. The model may repeat that data back — a privacy incident — and the prompt keeps growing until it exceeds the context window. The fix is history=None with a fresh list inside, or better, loading each user's history explicitly from storage.
Follow-up questions to expect
- "Why does Python evaluate defaults only once?" — Defaults are stored on the function object when
defruns; re-evaluating them per call would be a different language design. - "Is a tuple default safe?" — Yes, if it only holds immutable items, because nobody can modify it in place.
- "Is this ever used on purpose?" — Occasionally as a quick cache (
def f(x, _cache={})), butfunctools.lru_cacheis clearer.