Nothing is broken here. It is the one fact you need in order to see what is wrong on the next tab, so read the output rather than the code.
def note(text, log=[]):
log.append(text)
return log
print(note('first')) # -> ['first']
print(note('second')) # -> ['first', 'second'] <- the same list
print(note.__defaults__) # -> (['first', 'second'],)
# The [] was created once, when the def line ran — not once per call.
- A default is evaluated once, at definition time
Python runs the def statement once, builds the default value then, and stores it on the function object — you can literally print it out of __defaults__. Every later call that omits the argument gets that same object, not a fresh copy. For a number or a string that is invisible, because you cannot change them. For a list or a dict it is a shared bucket that outlives the call.
- The symptom always looks like data, never like syntax
Nobody reports 'the default argument is shared'. They report that a new record already has old data in it, that a counter never resets, that results from a previous user appear in this one’s response. When state leaks between calls that should be independent, look at the signature before you look at the logic.
- None is the sentinel, and the check goes inside
The fix is always the same shape: default to None, and build the real default inside the body, where it runs once per call. That is why you see `if thing is None:` at the top of so much library code — it is not defensive padding, it is this bug being avoided.
Both failing cases pass no basket at all. So the question is not what the function does with a basket — it is where the basket comes from when nobody supplies one.
The empty list in the signature is created once, when Python reads the def, and reused by every call that omits the argument. append() then mutates that one shared list forever.
Default the parameter to None. Inside the body, if it is None, use a new empty list. Then return a new list rather than appending to the caller’s — basket + [item] does both jobs at once.
def add_item(item, basket=____): if basket is ____: basket = [] return basket ____ [item]
def add_item(item, basket=None):
if basket is None:
basket = []
return basket + [item]
Any mutable default — list, dict, set — should be None in the signature with the real value built in the body. It is such a reliable rule that linters flag the alternative on sight (ruff calls it B006). Learn the shape rather than the individual cases: if the default can be changed, it must not live in the signature.
There were two problems here, and only one of them was the default. append() mutates whatever it is handed, so the caller’s basket changed as well. Concatenation builds a new list and leaves theirs alone. The 'leaves the caller’s basket alone' case exists to catch a fix that solves the famous half and misses this one.
`if not basket:` is true for None and also for an empty list — which happens to work here, but stops working the moment 'empty' and 'not supplied' need to mean different things. `is None` asks the precise question. Get in the habit while the two cases still agree.
An immutable default is completely safe and often clearer: def greet(name, greeting='hello') needs no sentinel, because nothing can change the string. The rule is about mutability, not about defaults. And occasionally a shared default is genuinely what you want — a memoisation cache pinned to the function is the classic example — but write that deliberately, with a comment saying so, because every reader will otherwise assume it is this bug.
- Write the same bug with a dict default and watch it behave identically, then check whether your linter catches both.
- Print add_item.__defaults__ before and after a few calls on the broken version, so you can see the shared list growing.
- Find out why Python evaluates defaults once rather than per call, and what it would cost if it did not — the answer is about how often the price would be paid.