You defined a function with bucket=[] as a default, called it twice, and watched accumulated values spill across calls. It looked like Python betrayed you. But the list was never meant to reset — it was created once when Python defined the function, stored on the function object, and reused on every call.
Think of it like an office coffee mug. One person fills it, drinks, and leaves it dirty. The next person finds yesterday's coffee and top it off. That is what mutable defaults do: they persist because the default expression runs only at definition time, not at call time.
Do not use [], {}, or other mutable objects as defaults for per-call state. Use None plus an inside-the-function allocation, or use a unique sentinel when None is itself meaningful.
# [CURRENT - 3.10-3.14] Works on Python 3.xclass Bus: def __init__(self, passengers=None): self.passengers = list(passengers) if passengers is not None else [] def pick(self, name): self.passengers.append(name)Default values are stored on the function object in __defaults__ and reused every time the caller omits that argument.
# [CURRENT - 3.10-3.14] Works on Python 3.xdef append_item(value, bucket=[]): bucket.append(value) return bucketprint(id(append_item.__defaults__[0]))print(append_item("a"), id(append_item.__defaults__[0]))print(append_item("b"), id(append_item.__defaults__[0]))The object identity stays the same across calls. That is the whole bug.
This is language semantics, baked into the function definition contract. __defaults__ merely makes it observable.
The one-time default-evaluation rule is stable Python 3 behavior and documented in the function definition reference. Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.
None is only a good sentinel if None is not a meaningful caller value. Otherwise create a dedicated object and compare with is.
# [CURRENT - 3.10-3.14] Works on Python 3.x_missing = object()def configure(value=_missing): if value is _missing: return "use inherited default" if value is None: return "disable feature" return f"set feature to {value!r}"If your constructor receives caller-owned mutable data, snapshot it when shared mutation is not intended.
# [CURRENT - 3.10-3.14] Works on Python 3.xclass Team: def __init__(self, members=None): self.members = list(members) if members is not None else []names = ["Ana"]team = Team(names)names.append("Bo")print(team.members) # ['Ana']Shared mutable defaults can be intentional for caches, but then they should be named, documented, bounded, and usually protected by synchronization in threaded code. Accidental shared state is the default bug.
For dataclasses, use field(default_factory=...) instead of a mutable literal. That moves allocation to instance creation time, not class definition time.
# [OLDER / 3.9, CURRENT - 3.10-3.14] Works on Python 3.9+ [PEP 585]from dataclasses import dataclass, field@dataclassclass Batch: rows: list[str] = field(default_factory=list)