Mutable Defaults
The last lesson said a default value fills a parameter the caller left out. True — but which value? A default is worked out once, when the def line runs, and the same one is handed to every call after that. With a number or a string nobody notices. With a list, everybody does.
1The program that surprises everyone
Here is a function that puts an item in a basket. If no basket is given, it starts an empty one:
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item('pen'))
print(add_item('book'))
print(add_item('bag'))['pen'] ['pen', 'book'] ['pen', 'book', 'bag']
Three separate calls, and the basket never emptied. Most people expect three lines each holding one item — ['pen'], ['book'], ['bag'] — because surely a fresh call makes a fresh basket?
basket name. It does not make a fresh list.2The list is made once, not once per call
Everything on the def line is worked out at the moment Python reads that line — and it reads it once. The [] is part of that line, so one empty list is created, once, and kept with the function for as long as the program runs.
You can watch it happen. Print the basket at the start of each call, before anything is added:
def add_item(item, basket=[]):
print('basket at the start of this call:', basket)
basket.append(item)
return basket
add_item('pen')
add_item('book')
add_item('bag')basket at the start of this call: [] basket at the start of this call: ['pen'] basket at the start of this call: ['pen', 'book']
It is empty on the first call only. By the second call the “empty basket” already has a pen in it, because it is the same basket the first call put the pen into. Nobody made a new one, because nothing asked for a new one to be made.
def add_item(item, basket=[]) does not mean “start with an empty list every time”. It means “start with this particular list, the one I made when I read the def line”. On the first call that list happens to be empty. After that, it is whatever the previous calls left in it.3The name is local. The list is not.
This is the sentence worth learning, so here it is slowly. A local variable is two things — a name and the object that name points at — and only the first of them belongs to the call.
add_item('pen')basket →add_item('book')basket →add_item('bag')basket →def ran[ ]One list, stored with the function itself. It is not inside any call, so no call can end and take it away.
Each call builds its own little workspace and puts the name basket in it. That name is thrown away when the call ends — but the name was only ever a label pointing at the list. The list itself lives with the function, not in the workspace, so it survives every call and carries the previous items with it.
And append does not touch the name at all. It reaches through the name and changes the list on the other end — the one every call is pointing at.
4Rebinding a name, and changing a thing
Python has two very different operations that both look like “the variable changed”, and telling them apart explains this whole lesson:
= signbasket = basket + [item]Builds a new list and points the name at that instead. The old list is left exactly as it was. The name has moved; nothing was edited.
appendbasket.append(item)Leaves the name where it is and edits the list it points at. Everyone else pointing at that list sees the change.
Swap append for an assignment and the surprise disappears completely — same default, same three calls:
def add_item(item, basket=[]):
basket = basket + [item] # a NEW list — the name now points at that
return basket
print(add_item('pen'))
print(add_item('book'))
print(add_item('bag'))['pen'] ['book'] ['bag']
Three separate answers, which is what everybody expected the first time. Print the basket at both ends of the call and you can see why:
def add_item(item, basket=[]):
print('start of call:', basket)
basket = basket + [item] # a NEW list — the name now points at that
print('end of call :', basket)
return basket
add_item('pen')
add_item('book')start of call: [] end of call : ['pen'] start of call: [] end of call : ['book']
print('start of call:', basket)Empty on BOTH calls now — the shared list is still empty, because the first call never changed it.
basket = basket + [item]The + builds a brand new list. The = points the local name at that new list, and lets go of the shared one.
print('end of call :', basket)The name is on the new list, so it shows one item. The shared list, sitting with the function, is untouched.
= redirects the name to something new and leaves the original alone. append, insert, remove, sort and basket[0] = … all reach through the name and edit the original, which is why the items pile up.This is the same mutable / immutable idea from Class 11, and the same one behind n = n + 1 changing nothing outside a function while marks.append(m) changes everything. One moves a label; the other edits a thing.
5Why msg='Hello' never caused trouble
Every default is shared — the string in msg='Hello' is created once too. It causes no trouble because a string cannot be edited in place. There is no msg.append(…) to write. The only thing you can do to a string is build a new one and rebind:
def greet(name, msg='Hello'):
msg = msg + '!' # rebinding — the only option a string allows
print(msg, name)
greet('Riya')
greet('Amit')Hello! Riya Hello! Amit
Both calls start from 'Hello', because nothing can reach into a string and change it. Numbers, strings, Booleans, None and tuples are all immutable, so all of them are safe as defaults. Lists, dictionaries and sets are not.
# a dictionary default has exactly the same problem
def tally(mark, marks={}):
marks[mark] = marks.get(mark, 0) + 1
return marks
print(tally('A'))
print(tally('B'))
print(tally('A')){'A': 1}
{'A': 1, 'B': 1}
{'A': 2, 'B': 1}6The fix: an immutable default, and a fresh list inside
If you want a genuinely new list on every call, the list has to be created inside the body — because the body is the part that runs every time. The default becomes None, which is immutable and therefore safe, and the first line checks for it:
def add_item(item, basket=None):
if basket is None:
basket = [] # runs on every call that needs it
basket.append(item)
return basket
print(add_item('pen'))
print(add_item('book'))
print(add_item('bag'))['pen'] ['book'] ['bag']
def add_item(item, basket=None):None is the default now. It is immutable, so there is nothing for the calls to share and spoil.
if basket is None:True only when the caller left the basket out. If they passed their own list, this is skipped.
basket = []Inside the body, so it runs afresh on every such call. THIS is the line that makes a new empty list each time.
is None rather than == None. Both work here, and is is what Python programmers write for this check. Read it as “is this the same nothing?”7Passing your own list is never affected
The sharing only happens when the argument is left out. Supply a basket and the default is not consulted at all:
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item('pen')) # uses the shared default
print(add_item('cap', ['own'])) # uses the caller's own list
print(add_item('book')) # back to the shared default['pen'] ['own', 'cap'] ['pen', 'book']
The middle call is completely separate. The last call carries on from ['pen'], because the shared list never saw the 'own' basket at all.
8Recap
The def line runs once, so the [] on it creates one list, kept with the function for the whole program.
Every call makes a new local name basket. Every one of those names points at that same one list.
basket = basket + [x] builds a new list and leaves the old one alone. basket.append(x) edits the shared one.
Numbers, strings, Booleans, None and tuples cannot be edited in place, so nothing can pile up in them.
def f(x, lst=None): then if lst is None: lst = []. The body runs every call, so the list really is new each time.
The default is only used when the argument is left out. Supply a list and it is yours alone.
- 1
Run the basket program and predict the fourth call before you add it.
Hint · It carries on from wherever the third call left off — four items, not one.
- 2
Print
basketat the start of the body and watch it arrive non-empty on the second call.Hint · That single line is the whole explanation. The “empty” default is not empty any more.
- 3
Change
basket.append(item)tobasket = basket + [item]and run it again.Hint · Three separate answers. The shared list is still there, just never touched.
- 4
Fix it with
basket=Noneand check all three calls give one item each.Hint · The
basket = []has to be inside the body, because the body is the part that runs every time. - 5
Write the same trap with a dictionary default and a
.get()tally.Hint · A dictionary is mutable too, so the counts keep adding up across calls.
Why does add_item('book') print ['pen', 'book'] and not ['book']?
Which change removes the surprise, keeping basket=[] as the default?
Why is msg='Hello' a perfectly safe default?
In the None fix, why must basket = [] be inside the body?