Python Part I · Module 5 · Lists
Changing a List: append, insert, remove, pop
This is the chapter the module exists for. Every string method in Module 4 handed back a new string and left yours untouched, because a string cannot be changed — and that rule was stated so firmly that it is worth saying plainly now that lists do not follow it. A list can be changed after it is created. You can replace an element, add one, take one out, and the list you already have is afterwards a different list. Nothing is returned that you need to catch, and no name needs reassigning. That difference is simple to state and takes real care to use correctly, because one of the most common beginner bugs in Python comes from assuming these methods behave like the string ones.
Foundation16 min readPythonPart IFundamentalsListsMutation
Part I · Module 5 · Chapter 5.3 · Changing a List: append, insert, remove, pop
1. The Problem
A script finds out things as it goes. It starts knowing nothing about which tests failed, and by the end it needs a list of them. A report starts empty and gains rows.
That is impossible with the tools you have so far. Everything Module 4 taught produced a new value: line.strip() gave you a new string, ",".join(parts) gave you a new string. To grow a collection you would have to rebuild it every time.
Lists work differently, and this chapter is that difference.
2. Replacing an Element
Here is the demonstration that makes the word "mutable" mean something. Assign to a position, exactly as you would read from one:
tests = ["smoke", "reset", "interrupt"]
print(tests)
tests[1] = "boot"
print(tests)['smoke', 'reset', 'interrupt']
['smoke', 'boot', 'interrupt']Position 1 held "reset" and now holds "boot". Nothing was reassigned — there is no tests = on that middle line. The list itself changed.
Now put that beside the string it is the opposite of. Chapter 4.11 established this and it is worth seeing the two together:
text = "PASS"
text[0] = "F"TypeError: 'str' object does not support item assignmentIdentical syntax. On a list it works; on a string it is a TypeError, because a string has no operation for having one of its items replaced. That is the whole of the mutable/immutable distinction Chapter 2.13 introduced, now visible in two lines of code.
This chapter uses one grammar throughout, and it is worth naming because it is the only reliable way to reason about mutation: print the list, do the operation, print it again. Before and after. When a list changes in place, the change is only visible if you look.
3. Mutation Is Not Rebinding
These two look similar and are completely different operations. Both end with a referring to a list starting with "boot":
a = ["smoke", "reset"]
a[0] = "boot"
print(a)['boot', 'reset']b = ["smoke", "reset"]
b = ["boot"]
print(b)['boot']The first mutated a list: the same list value that a referred to before now has a different element at position 0. The second rebound a name: a brand-new list was built and b was pointed at it, and the old list is simply no longer what b refers to.
The difference is invisible when only one name is involved — which is exactly why it is worth establishing now, before it starts mattering:
tests = ["smoke", "reset"]
before = tests
tests[0] = "boot"
print(tests)
print(before)
print(tests == before)['boot', 'reset']
['boot', 'reset']
TrueLook at the second line of output. before was never assigned to again, and it shows "boot" as well.
That is not a mistake, and it is not something to work around. before = tests did not take a snapshot — it gave a second name to the same list, and mutating that list is visible through either name. Chapter 5.8 is devoted to this, and it is one of the most useful things in Part I. For now, notice only that mutation is visible everywhere the list is reachable, and rebinding is not.
One failure worth seeing, because the message is specific:
tests = ["smoke", "reset"]
tests[5] = "x"IndexError: list assignment index out of rangeAssignment reaches an existing position; it does not create one. A two-element list has positions 0 and 1, and there is no position 5 to assign to. Note list assignment index out of range rather than the plain list index out of range from Chapter 5.2 — Python is telling you which operation failed. To make the list longer you add to it, which is the next section.
4. append(): One Element on the End
The most common list operation there is. A script starts with nothing and accumulates:
failures = []
print(failures)
failures.append("axi_burst")
print(failures)
failures.append("reg_rw")
print(failures)[]
['axi_burst']
['axi_burst', 'reg_rw']append is read exactly as Chapter 4.1 taught method calls: ask this list to perform its append operation with this value. The list gains one element on the end, and the list you already had is the one that changed.
The word one is load-bearing:
tests = ["smoke"]
tests.append(["reset", "burst"])
print(tests)
print(len(tests))['smoke', ['reset', 'burst']]
2That is not three tests. append was handed one value — a list — so it added one element, and that element happens to be a list. The result has two elements: a string and a list. Chapter 5.6 covers the operation that adds each of several values instead, and Chapter 5.7 covers lists that hold lists deliberately.
append takes exactly one value, and says so if given more:
tests = []
tests.append("a", "b")TypeError: list.append() takes exactly one argument (2 given)5. The Trap: append() Returns None
This is the most expensive misunderstanding in the module, and it deserves the space.
append changes the list. It does not hand the list back:
tests = ["smoke", "reset"]
result = tests.append("interrupt")
print(result)
print(tests)None
['smoke', 'reset', 'interrupt']The list gained its element — look at the second line. But result is None.
Now the version that breaks real scripts, because it looks exactly like correct Module 4 code:
tests = ["smoke", "reset"]
tests = tests.append("interrupt")
print(tests)
print(type(tests))None
<class 'NoneType'>No error. No warning. tests is now None, and the next thing that touches it fails somewhere else entirely with a message that says nothing about this line.
It is worth being precise about why, because "append returns None" stated as a bare fact does not stop anyone writing this. Three things happen, in order:
tests.append("interrupt")mutates the list. The list genuinely gains its element — that part works.- The expression
tests.append("interrupt")evaluates toNone. Chapter 3.1 established that every expression produces a value; this one producesNonebecause there is nothing useful to hand back — the change already happened to the list you have. tests = ...then rebinds the name to thatNone, exactly as Chapter 2.3 says assignment does. The list still exists and is still correct;testsjust no longer refers to it.
So the list was never the problem. The assignment was.
This is the precise opposite of the habit Module 4 spent thirteen chapters building. There, a method call alone on a line did nothing and you had to store the result. Here, storing the result is the bug. The rule that covers both:
An immutable value's methods return a new value — keep it. A mutable value's methods change the value — do not.
6. insert(): Putting One in the Middle
append only ever adds at the end. When position matters, insert takes the position first and the value second:
tests = ["smoke", "interrupt"]
print(tests)
tests.insert(1, "reset")
print(tests)['smoke', 'interrupt']
['smoke', 'reset', 'interrupt']Read insert(1, "reset") as put this value at position 1. Afterwards "reset" is at position 1, and — this is the part to be clear about — "interrupt" did not disappear. It moved from position 1 to position 2. The list is now three elements long:
tests = ["smoke", "interrupt"]
tests.insert(1, "reset")
print(len(tests))3insert adds; it never replaces. Replacement is tests[1] = "reset" from Section 2, which keeps the length the same. Those two are easy to confuse and do different things.
The boundaries are forgiving:
t2 = ["b", "c"]
t2.insert(0, "a")
print(t2)
t2.insert(99, "z")
print(t2)['a', 'b', 'c']
['a', 'b', 'c', 'z']Position 0 puts the element at the front. A position past the end does not raise — it appends. That is deliberate, but it means a wrong position number gives you a quietly misplaced element rather than an error.
7. remove(): Taking Out a Value You Name
remove is given a value, not a position, and takes out the first element equal to it:
statuses = ["PASS", "FAIL", "PASS"]
print(statuses)
statuses.remove("PASS")
print(statuses)['PASS', 'FAIL', 'PASS']
['FAIL', 'PASS']Two things to read carefully there. The list started with three elements and ends with two — so one was removed, not both matches. And the one removed was the first "PASS", which is why the survivor is at the end.
Like every mutating method here, it returns nothing useful:
s2 = ["a", "b"]
r = s2.remove("a")
print(r, s2)None ['b']And asking it to remove something that is not there is an error rather than a silent no-op:
tests = ["smoke"]
tests.remove("reset")ValueError: list.remove(x): x not in listA ValueError, not an IndexError — the argument was a perfectly good value, it just was not in the list. The message even shows you the method's shape. Until Module 9 gives you if, the way to be safe is to check membership with in from Chapter 4.6 before removing.
8. pop(): Remove and Hand It Back
pop is the one mutating method in this chapter that returns something you want. It removes an element and gives it to you:
tests = ["smoke", "reset", "interrupt"]
removed = tests.pop()
print(removed)
print(tests)interrupt
['smoke', 'reset']With no argument it takes the last element. removed is that element, and the list is now shorter.
Give it a position and it takes that one instead:
t3 = ["smoke", "reset", "interrupt"]
first = t3.pop(0)
print(first)
print(t3)smoke
['reset', 'interrupt']So pop is the method where assignment is correct: removed = tests.pop() is right, and tests = tests.pop() is the bug — it would set tests to the removed element.
Both failures are worth seeing, because the messages differ:
print([].pop())IndexError: pop from empty listprint(["a"].pop(5))IndexError: pop index out of rangeThe first is the one that bites in practice: a list of failures is legitimately empty when nothing failed, and popping from it raises.
9. clear(), and the Pattern All of This Is For
clear empties a list in place. The list still exists and is still the same list — it now has no elements:
t4 = ["a", "b"]
t4.clear()
print(t4, len(t4))[] 0Now the pattern the canonical curriculum names for this chapter, and the reason append matters more than the rest put together. A script begins knowing nothing and accumulates as it learns:
failures = []
failures.append("axi_burst")
failures.append("reg_rw")
print(failures)
print(len(failures))['axi_burst', 'reg_rw']
2Start empty, add as you find, report at the end. That is the shape of nearly every checking script ever written.
What is missing is the part that decides when to append — a script does not know its failures in advance, it works through results and adds the ones that failed. Deciding is if (Module 9) and working through is a loop (Module 10). This chapter owns the accumulation itself, which is the piece that does not change when those arrive: failures = [] before, failures.append(...) as you go, len(failures) after.
10. What Each Operation Does
Now that each has arrived from a problem, here they are together. This is a summary of what you have seen, not the lesson:
| Operation | Changes the list? | Evaluates to | Notes |
|---|---|---|---|
tests[1] = "boot" | yes — replaces | — | length unchanged; position must exist |
tests.append(x) | yes — adds one at the end | None | one element, whatever x is |
tests.insert(i, x) | yes — adds at position i | None | shifts the rest along; never replaces |
tests.remove(x) | yes — takes out first match | None | ValueError if absent |
tests.pop() | yes — takes off the last | the element | pop(i) for a position |
tests.clear() | yes — empties it | None | same list, no elements |
The only row where assignment is correct is pop.
11. Predict, Run, Observe
Predict every printed line before reading on. Three of these are the traps from this chapter.
jobs = ["smoke", "reset", "smoke"]
jobs.append("burst")
print(jobs)
jobs.remove("smoke")
print(jobs)
last = jobs.pop()
print(last)
print(jobs)
jobs.insert(0, "boot")
print(jobs)
print(jobs.append("x"))
print(len(jobs))The verified output:
['smoke', 'reset', 'smoke', 'burst']
['reset', 'smoke', 'burst']
burst
['reset', 'smoke']
['boot', 'reset', 'smoke']
None
4Follow it through. append("burst") puts one element on the end. remove("smoke") takes out the first "smoke" only — the second survives, which is Section 7. pop() returns "burst" and shortens the list. insert(0, "boot") puts an element at the front and shifts the rest.
The sixth line is None because print(jobs.append("x")) prints what the call evaluates to, not the list — Section 5. And the last line is 4, not 3: that append really did add "x" to the list even though the print showed None. Both things are true at once, and that is exactly why the trap is dangerous.
12. Exercises
Exercise 1 — before and after. For signals = ["clk", "valid"], write the four statements that: add "ready" at the end, insert "reset_n" at position 1, replace "valid" with "vld", and remove "clk". Print the list after each, and predict each result before running.
Exercise 2 — mutation or rebinding. For each, say which it is and what the list contains afterwards:
(a) a[0] = "x" (b) a = ["x"] (c) a.append("x") (d) a = a + ["x"]
Exercise 3 — the None trap. This prints None and then fails a few lines later. Explain the three steps that produced None, and fix it with the smallest possible change:
failures = []
failures = failures.append("axi_burst")
print(failures)Exercise 4 — first match only. Given results = ["FAIL", "PASS", "FAIL"], predict the list after one results.remove("FAIL"). Then say how many calls it would take to remove both, and why you cannot simply call it twice on a list where you are unsure how many there are.
Exercise 5 — insert or replace. A colleague wants "reset" to become "boot" in ["smoke", "reset", "burst"] and writes tests.insert(1, "boot"). Say what they actually get, why, and give the line they meant.
Exercise 6 — pop is different. Explain in two sentences why removed = tests.pop() is correct while tests = tests.append(x) is a bug, referring to what each method evaluates to.
Exercise 7 — the empty list. Write the two lines that would make pop() raise, name the exact error, and say when that situation legitimately happens in a real script.
Exercise 8 — one element or several. Predict len(tests) after each, starting from tests = ["smoke"]:
(a) tests.append("reset") (b) tests.append(["reset", "burst"]) (c) tests.insert(0, "boot")
13. Summary
- A list can be changed after it is created. That is what mutable means, and it is the opposite of the string rule from Chapter 4.11.
tests[1] = "boot"replaces an element in place. The identical syntax on a string is aTypeError.- Mutation changes the value; rebinding changes what a name refers to.
tests[0] = xandtests = [x]are different operations. - Assignment cannot extend a list —
tests[5] = xon a two-element list isIndexError: list assignment index out of range. append(x)adds exactly one element to the end, whateverxis. Appending a list adds one element that is a list.appendevaluates toNone.tests = tests.append(x)setsteststoNone— silently. Write the call on its own line.- An immutable value's methods return a new value, so keep it. A mutable value's methods change the value, so do not.
insert(i, x)adds at a position and shifts the rest. It never replaces, and a position past the end appends rather than raising.remove(x)takes out the first match only, and raisesValueError: list.remove(x): x not in listif absent.pop()removes and returns the element — the one method here where assignment is right.pop()on an empty list isIndexError: pop from empty list.clear()empties the list in place.- Start empty, append as you find, report at the end — the accumulation pattern that most checking scripts are built from.
Mutation changes what is in a list. The next chapter puts what is in it into a useful order.
Where this fits
Part of the Python curriculum.
