Lists, Mutation, Methods, and Aliasing
Key Takeaways
- Lists are mutable, so item assignment, slice assignment, deletion, and many list methods change the existing list object.
- Methods such as append, extend, insert, sort, reverse, and clear mutate the list in place and return None.
- pop mutates the list and returns the removed element, while remove mutates the list and returns None.
- Assignment such as second = first creates another name for the same list object rather than a copy.
- Use slicing, list.copy(), or list() for a shallow copy when later top-level list changes should not affect the original.
Mutable sequence behavior
A list is an ordered, mutable collection. Ordered means positions matter. Mutable means the same list object can be changed after it is created. PCEP questions use this to test whether you can separate the list object from the variable names that refer to it.
scores = [70, 80, 90]
scores[1] = 85
print(scores) # [70, 85, 90]
The assignment did not create a new list. It changed the object that scores already referenced. Lists can also change through slice assignment, del, and list methods.
Method effects and return values
Many beginner mistakes come from assuming that a method returns the changed list. In Python, the common in-place list methods usually return None. They change the list object directly.
| Method | Mutates list? | Return value | Typical use |
|---|---|---|---|
append(x) | Yes | None | Add one element at the end |
extend(iterable) | Yes | None | Add each element from another iterable |
insert(i, x) | Yes | None | Insert one element before index i |
remove(x) | Yes | None | Remove first matching value |
pop() | Yes | Removed element | Remove and return last item |
sort() | Yes | None | Sort the existing list |
reverse() | Yes | None | Reverse the existing list |
clear() | Yes | None | Remove all elements |
This code is a classic trap:
values = [3, 1, 2]
result = values.sort()
print(values) # [1, 2, 3]
print(result) # None
If you need a new sorted list, use sorted(values). If you use values.sort(), expect the original list to change and the method call itself to evaluate to None.
append versus extend
append() adds one object. extend() iterates through another iterable and adds each element. With strings, that distinction is especially visible.
letters = ['a']
letters.append('bc')
print(letters) # ['a', 'bc']
letters = ['a']
letters.extend('bc')
print(letters) # ['a', 'b', 'c']
Aliasing
Assignment does not copy a list. It gives the same list another name.
first = [1, 2]
second = first
second.append(3)
print(first) # [1, 2, 3]
Both names point at the same object, so mutation through either name is visible through the other. To make a separate top-level list, use a shallow copy:
original = [1, 2]
copy_a = original[:]
copy_b = original.copy()
copy_c = list(original)
A shallow copy is enough when the elements are immutable values such as numbers and strings. It is not a full solution for nested lists, because the inner lists are still shared.
grid = [[1], [2]]
copy = grid[:]
copy[0].append(9)
print(grid) # [[1, 9], [2]]
Exam strategy
For list questions, mark each operation as either a mutation, a new object creation, or a read. Then write down the method return value. That two-column trace quickly reveals why x = x.append(5) makes x become None, why pop() can be printed, and why aliases see each others mutations.
What is printed by this code? nums = [3, 1, 2]; x = nums.sort(); print(x)
What is printed? a = [1, 2]; b = a; b.append(3); print(a)
Which list method both changes the list and returns the removed element?