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 written with square brackets. Ordered means positions matter and indexing/slicing work like any sequence. Mutable means the same list object can be changed after creation. PCEP uses 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 also change through slice assignment and del.
letters = ['a', 'b', 'c', 'd']
letters[1:3] = ['X'] # slice assignment can change length
print(letters) # ['a', 'X', 'd']
del letters[0]
print(letters) # ['X', 'd']
Slice assignment is powerful: the right side is iterated, so letters[1:1] = [9, 8] inserts without removing anything, and the replacement does not need to match the slice length.
Method effects and return values
Many beginner mistakes come from assuming a method returns the changed list. In Python the common in-place list methods 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 an iterable |
insert(i, x) | Yes | None | Insert one element before index i |
remove(x) | Yes | None | Remove first matching value (ValueError if absent) |
pop() / pop(i) | Yes | Removed element | Remove and return last (or i-th) item |
sort() | Yes | None | Sort the existing list |
reverse() | Yes | None | Reverse the existing list |
clear() | Yes | None | Remove all elements |
index(x) | No (read) | int position | Find first index of a value |
count(x) | No (read) | int count | Count occurrences |
This snippet 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 without changing the original, use the built-in sorted(values), which returns a new list. values.sort() mutates in place and evaluates to None.
append versus extend
append() adds one object as a single element. extend() iterates through another iterable and adds each element separately. With strings the distinction is especially visible because a string iterates character by character.
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. Both names point at one object, so mutation through either name is visible through the other.
first = [1, 2]
second = first
second.append(3)
print(first) # [1, 2, 3]
To make a separate top-level list, take 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 between the copies.
grid = [[1], [2]]
copy = grid[:]
copy[0].append(9)
print(grid) # [[1, 9], [2]]
Exam strategy
For list questions, label each operation as a mutation, a new-object creation, or a read, then write down the method's return value. That two-column trace instantly reveals why x = x.append(5) makes x become None, why print(stack.pop()) shows the removed element, and why two aliases see each other's mutations. Watch for + versus +=: a = a + [1] builds a brand-new list, while a += [1] mutates the existing list in place (like extend).
List construction, comprehensions, and the in-place trap
PCEP expects you to recognize several ways a list comes into existence and how each interacts with aliasing. A literal such as [1, 2, 3] builds a fresh list. The list() constructor turns any iterable into a list, so list('abc') gives ['a', 'b', 'c'] and list((1, 2)) gives [1, 2]. A list comprehension like [n * 2 for n in range(3)] produces [0, 2, 4] and is itself a brand-new list each time it runs. The reason these matter is that whether a line creates a new object or mutates an existing one determines what every alias sees afterward.
Consider the multiplication trap with nested lists: grid = [[0]] * 3 does not make three independent inner lists; it makes three references to the same inner list, so grid[0].append(9) changes all three rows to [[0, 9], [0, 9], [0, 9]]. By contrast, [[0] for _ in range(3)] builds three separate inner lists. On the exam, when you see repetition (*) applied to a list that contains mutable elements, immediately suspect shared references; when you see a comprehension, expect independent objects.
Finally, remember that calling a mutating method as part of a larger expression substitutes None into that expression, so sorted_first = [3, 1].sort()[0] raises TypeError because .sort() returned None and you cannot index None.
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?