Lessons · Python · the def f(x=[]) trap
The default argument that remembers
A default value is created once, when the function is defined. If it is a list or dict, every call shares the same one.
Hone is a place to practise programming. This is one of its lessons, written out in full and free to read without an account.
What it is for
This is a genuine production bug: a cache or history that quietly carries data from one request into the next. It passes every simple test and fails under real use.
How to think about it
Ask one question of any default you meet: can this thing be changed in place? A list, a dict and a set all can, and all three are shared by every call. Swap the default for None, then make the real one on the first line of the body. The type you build there is whatever that function actually needs -- a list, a dict, a set -- and the guard reads the same either way.
Worked example
def tally(word, seen={}):Written the way it is usually written. The default LOOKS like it is made fresh on each call.seen[word] = seen.get(word, 0) + 1Nothing is wrong on this line. The bug is on the one above it.
return seen
print(tally('apple')){'apple': 1}print(tally('pear')){'apple': 1, 'pear': 1}def tally(word, seen=None):The repair. None cannot be filled up, so there is nothing to carry over.
if seen is None: seen = {}A call with no dictionary now builds its own, every time.seen[word] = seen.get(word, 0) + 1
return seen
print(tally('apple')){'apple': 1}print(tally('pear')){'pear': 1}Your turn
Fix the default so calls do not share state.
def log(msg, history=):
if history is None:
history = []
history.append(msg)
return historySolve one with the tests running
The trap
def f(items=[]) looks right and works the first time. The second call finds the first call's items still there.