Changing a list, and the copy you thought you made
Methods that change the list in place
items = ["rice", "dal"]
items.append("oil") # ['rice', 'dal', 'oil']
items.extend(["salt", "tea"]) # adds each element
items.insert(0, "atta") # at position 0, everything shifts right
items.remove("dal") # removes the first match; ValueError if absent
last = items.pop() # removes and returns the last
second = items.pop(1) # removes and returns position 1
items.sort() # sorts in place
items.reverse() # reverses in placeappend adds one item. extend adds each item of an iterable. The difference is the most common list bug in beginner code:
a = [1, 2]
a.append([3, 4]) # [1, 2, [3, 4]] — three items, one of them a list
a = [1, 2]
a.extend([3, 4]) # [1, 2, 3, 4] — four itemsa + [3, 4] behaves like extend but builds a new list rather than modifying a.
The None that catches everybody
scores = [3, 1, 2]
scores = scores.sort()
print(scores) # Nonesort() sorts the list and returns None, deliberately, as a signal that it changed the thing rather than producing a new thing. Assigning its result throws the list away. The same is true of append, extend, insert, reverse and remove.
When you want a new sorted list and the original left alone, use the function, not the method:
ordered = sorted(scores) # returns a new list; scores unchangedThe general rule in Python: a method that mutates returns None; a function or method that computes something returns the value. Once you see the pattern, AttributeError: 'NoneType' object has no attribute ... becomes instantly diagnosable.
Two names, one list
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4]b = a did not copy anything. It attached a second name to the same list object. Every list method called through either name is visible through both. This is not a quirk of lists — it is how names work for every mutable object in Python — but lists are where people meet it.
To actually copy:
b = a.copy() # or list(a), or a[:]The shallow copy trap
All three of those are shallow. They copy the outer list; the inner objects are shared.
grid = [[0, 0], [0, 0]]
copy = grid.copy()
copy[0][0] = 9
print(grid) # [[9, 0], [0, 0]] — the original changedThe outer lists are separate, so copy.append(...) would not touch grid. But copy[0] and grid[0] are the same inner list. For nested structures you need a deep copy:
import copy as copy_module
independent = copy_module.deepcopy(grid)deepcopy walks the whole structure and rebuilds every level. It is slower, and on a large nested structure noticeably so, which is why it is not the default.
A related trap creates the same problem at construction time:
grid = [[0] * 3] * 2 # looks like a 2x3 grid
grid[0][0] = 9
print(grid) # [[9, 0, 0], [9, 0, 0]]* 2 repeated the reference, not the row. Build rows with a comprehension instead: [[0] * 3 for _ in range(2)].
Never remove from a list you are looping over
numbers = [1, 2, 2, 3, 4]
for n in numbers:
if n == 2:
numbers.remove(n)
print(numbers) # [1, 2, 3, 4] — one 2 survivedThe loop keeps an internal position. Removing the item at position 1 shifts everything left, so the next step to position 2 skips over the item that just moved into position 1. Nothing errors. You get a plausible, wrong answer, and only when duplicates are adjacent, which is why it survives testing.
Two safe fixes:
numbers = [n for n in numbers if n != 2] # build a new list — usually best
for n in numbers[:]: # or iterate over a copy
if n == 2:
numbers.remove(n)The same rule applies to dictionaries: changing the size of a dict while iterating over it raises RuntimeError: dictionary changed size during iteration. Lists do not even give you that courtesy.
What this costs
append and pop() from the end are fast, effectively constant time. insert(0, x) and pop(0) shift every remaining element, so they cost time proportional to the length of the list. Building a queue by repeatedly popping position 0 of a 100,000-item list is quadratic and will hang. collections.deque exists exactly for that: popleft() on a deque is constant time.
x in some_list also scans the whole list. If you check membership in a loop, the next lesson has the container you want.
The one thing to keep
Assignment attaches a second name to the same list, `copy()` duplicates only the outer level, and a mutating method returns None rather than the list.
Before you move on
A program builds `template = [[""] * 5] * 3` for a three-row form, then sets `template[1][0] = "name"` and finds all three rows now contain "name" in the first column. What happened?
Pick the one you would defend. Nobody sees your answer.