Output Puzzles: Lists, Aliasing and the Default-Argument Trap
45 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Lists are mutable, and that is the source of the most famous Python surprises. Two names can share one list, a copy can still share inner lists, and a default argument can remember earlier calls. Trace which object each name points to.
Names, not boxes
Aliasing means two or more names refer to the same object. With a mutable object such as a list, a change made through one name is visible through every other name.
b = a does not copy a list. It makes a second name for the same list. Changing it through either name is seen by both.
a = [1, 2]b = ab.append(3)print(a)c = a[:]c.append(4)print(a, c)print(a is b, a is c)This prints [1, 2, 3], then [1, 2, 3] [1, 2, 3, 4], then True False. A slice a[:] makes a new list.
Shallow copies and nested lists
A shallow copy is a new outer container that holds the same inner objects as the original. Changing an inner list shows through both copies; use copy.deepcopy to copy inner objects as well.
A copy made with [:], list() or .copy() is shallow. It has a new outer list, but the inner lists are still shared. copy.deepcopy copies the inner lists too.
import copy
rows = [[1], [2]]shallow = list(rows)deep = copy.deepcopy(rows)rows[0].append(9)print(shallow, deep)grid = [[0] * 2] * 2grid[0][0] = 1print(grid)The same trap hides in [[0] * 2] * 2, which repeats one inner list.
Example 1 · Shared rows in a grid
bad = [[0] * 3] * 2bad[0][1] = 5print(bad)
good = [[0] * 3 for _ in range(2)]good[0][1] = 5print(good)[[0, 5, 0], [0, 5, 0]] [[0, 5, 0], [0, 0, 0]]
What this code does
*repeats the same inner list, so both rows change.- A comprehension builds a new inner list for every row.
Example 2 · Removing items while looping
nums = [2, 4, 6, 7]for n in nums: if n % 2 == 0: nums.remove(n)print(nums)
nums = [2, 4, 6, 7]nums = [n for n in nums if n % 2 != 0]print(nums)[4, 7] [7]
What this code does
- Removing shifts the later items left, so the loop skips the next one.
- Build a new list instead of changing the one you loop over.
This prints [[1, 9], [2]] [[1], [2]], then [[1, 0], [1, 0]]. Both rows of grid are the same list.
Methods that return None
Methods that change a list in place, such as append, sort, reverse and extend, return None. sorted() returns a new list and leaves the original alone.
nums = [3, 1, 2]print(sorted(nums), nums)print(nums.sort())print(nums)This prints [1, 2, 3] [3, 1, 2], then None, then [1, 2, 3]. Writing nums = nums.sort() would replace the list with None.
The mutable default argument
A mutable default argument is a list, dict or set used as a parameter default. It is created once when the function is defined, so every call that uses the default shares and changes the same object.
A default value is created once, when the def line runs, not on every call. If the default is a list, all calls that rely on it share one list.
def add(x, bag=[]): bag.append(x) return bag
add(1)print(add(2))
def add_safe(x, bag=None): if bag is None: bag = [] bag.append(x) return bag
add_safe(1)print(add_safe(2))Example 3 · A default list that remembers
def add_order(order, orders=[]): orders.append(order) return orders
print(add_order("A1"))print(add_order("B2"))print(add_order("C3", []))['A1'] ['A1', 'B2'] ['C3']
What this code does
- The first two calls share one list created at definition time.
- Passing a fresh list as the third argument gives a separate result.
Example 4 · += versus + on a shared list
a = [1, 2]b = ab += [3]c = ac = c + [4]print(a, c)[1, 2, 3] [1, 2, 3, 4]
What this code does
+=extends the list in place, soasees the change.c + [4]builds a new list and rebindsc, soais not changed again.
The first print shows [1, 2], because the second call reuses the list from the first. The safe version prints [2]. Use None as the default and create the list inside.
Next step
Check your understanding
12 quick questions