Python Part I · Module 5 · Lists
Combining and Repeating Lists
You have two lists of tests and want one. You want a list of ten zeros without typing ten zeros. Both are ordinary needs and Python handles them with the same two operators that joined and repeated strings back in Chapter 4.3 — which means the notation costs you nothing to learn. What does need care is the question this module keeps asking: does this change the list I have, or build a new one? Combining lists can be done either way, and the two ways look almost identical on the page. This chapter separates them, finishes the append comparison Chapter 5.3 started, and meets a repetition trap that has caught a very large number of people.
Foundation13 min readPythonPart IFundamentalsListsOperators
Part I · Module 5 · Chapter 5.6 · Combining and Repeating Lists
1. Plus Builds a New List
+ between two lists produces a list containing the elements of the first followed by the elements of the second — exactly as it did for strings in Chapter 4.3:
a = [1, 2]
b = [3, 4]
c = a + b
print(c)
print(a)
print(b)[1, 2, 3, 4]
[1, 2]
[3, 4]Three lists now exist. The important lines are the last two: a and b are unchanged. + built something new and left both operands alone, which is the same behaviour every operator in Module 3 had.
That makes + the safe choice when the original lists still matter.
Both operands have to be lists. Python will not combine a list with something else:
print([1, 2] + "ab")TypeError: can only concatenate list (not "str") to listprint([1, 2] + 3)TypeError: can only concatenate list (not "int") to listCompare that message with the one from Chapter 4.3 — can only concatenate str (not "int") to str. Same sentence, different type. To add a single value to a list, either put it in a list of its own ([1, 2] + [3]) or use append from Chapter 5.3.
2. extend() Changes the List You Have
extend is the mutating alternative. It adds every element of one list onto the end of another, and the list you called it on is what changes:
a2 = [1, 2]
b2 = [3, 4]
print(a2)
a2.extend(b2)
print(a2)
print(b2)[1, 2]
[1, 2, 3, 4]
[3, 4]Before and after on a2. And b2 — the list handed to extend — is untouched; only the receiver changes.
Being a mutating method, it behaves like every other one from Chapter 5.3:
x = [1]
print(x.extend([2]), x)None [1, 2]None, and the list changed. So a = a.extend(b) sets a to None, which is the same trap in a new place.
3. append() and extend(): The Shape Difference
Chapter 5.3 showed that append adds exactly one element, and noted that the alternative would arrive later. This is it, and the difference is far easier to see than to define:
p = ["smoke"]
p.append(["reset", "burst"])
print(p, len(p))['smoke', ['reset', 'burst']] 2q = ["smoke"]
q.extend(["reset", "burst"])
print(q, len(q))['smoke', 'reset', 'burst'] 3Same starting list, same argument, completely different results.
append was handed one value and added one element — and that element is a list, sitting inside the outer one. Two elements total.
extend was handed a list and added each of its elements separately. Three elements total, all strings, nothing nested.
There is one surprise worth knowing, and it is not an error, which makes it worse:
x = ["a"]
x.extend("bc")
print(x)['a', 'b', 'c']extend was given a string rather than a list, and it added each of its characters. That follows from the rule above — a string supplies its characters one at a time, exactly as Chapter 4.9 showed when join did the same thing. If you meant to add one element, append("bc") is the call.
4. The Same Choice, Again
You have now met this decision three times, and it is worth naming as a pattern rather than three separate facts:
| Build a new value | Change the one you have |
|---|---|
a + b | a.extend(b) |
sorted(a) | a.sort() |
a[:] (Chapter 5.2) | — |
Everything in the left column leaves your list alone and hands you something; everything in the right column changes your list and hands you None.
5. Two Lines That Look the Same and Are Not
This is the most valuable thing in the chapter, and it follows directly from Section 4.
Chapter 3.3 taught +=. On a list it mutates:
m = [1, 2]
n = m
m += [3]
print(m)
print(n)[1, 2, 3]
[1, 2, 3]Both names show the change, because there is one list and += changed it.
Now the version that looks equivalent:
m2 = [1, 2]
n2 = m2
m2 = m2 + [3]
print(m2)
print(n2)[1, 2, 3]
[1, 2]n2 is different. Here m2 + [3] built a new list — Section 1 — and m2 = rebound the name to it. The original list was never changed, and n2 still refers to it.
So m += [3] and m = m + [3] produce the same value for m and do genuinely different things to everything else. The first mutates; the second rebinds.
6. Repeating a List
* with a whole number repeats a list, just as it repeated a string:
print([0] * 3)
print(["-"] * 4)[0, 0, 0]
['-', '-', '-', '-']This is the normal way to build a list of a known length filled with a starting value — ten zeros to count into, say, rather than typing them out.
It builds a new list and leaves the original alone:
z = [1, 2]
w = z * 2
print(z, w)[1, 2] [1, 2, 1, 2]And the edges behave as Chapter 4.3 established for strings — zero or a negative count gives an empty result, quietly:
print([0] * 0)
print([0] * -2)[]
[]Which is worth remembering, because a repetition count that comes out negative gives you an empty list rather than a complaint.
7. The Repetition Trap
Now a warning, because you will try this the first time you need a grid.
Repeating a list whose element is itself a list does something that looks right and is not:
rows = [[0] * 3] * 2
print(rows)[[0, 0, 0], [0, 0, 0]]That looks exactly like two rows of three zeros. Now change one value in the first row:
rows = [[0] * 3] * 2
rows[0][0] = 9
print(rows)[[9, 0, 0], [9, 0, 0]]Both rows changed. One assignment, two rows different.
The reason is that * 2 did not make two rows. It made an outer list with two elements, and both of those elements are the same inner list — repetition repeats the element you gave it, and the element was one list. Changing it through the first position is changing it through the second position, because there is only one inner list to change.
Written out row by row, it behaves as expected:
safe = [[0] * 3, [0] * 3]
safe[0][0] = 9
print(safe)[[9, 0, 0], [0, 0, 0]]Here [0] * 3 was evaluated twice, producing two separate inner lists, so changing one leaves the other alone.
8. Predict, Run, Observe
Predict every line before reading on. Two of these are the traps from Sections 5 and 7.
a = ["smoke"]
b = ["reset", "burst"]
print(a + b)
print(a)
a.extend(b)
print(a)
print(len(a))
c = ["x"]
c.append(["y", "z"])
print(c, len(c))
d = [1, 2]
e = d
d = d + [3]
print(e)The verified output:
['smoke', 'reset', 'burst']
['smoke']
['smoke', 'reset', 'burst']
3
['x', ['y', 'z']] 2
[1, 2]Line 2 is the point of +: after a + b, a is still just ['smoke']. Line 3 shows extend doing the same combination by changing a instead.
c.append(["y", "z"]) gives a two-element list with a list inside it — Section 3.
And the last line is [1, 2], not [1, 2, 3]. d = d + [3] built a new list and rebound d; e still refers to the original, unchanged. Had that line been d += [3], e would have shown [1, 2, 3]. That one-character difference is Section 5.
9. Exercises
Exercise 1 — combine two ways. Given first = ["smoke", "reset"] and second = ["burst"], write the line that produces a combined list without changing first, and separately the line that adds second's elements into first. Print first after each.
Exercise 2 — predict the shape. Starting from t = ["a"] each time, give the value and len() after each, then verify:
(a) t.append("b") (b) t.append(["b", "c"]) (c) t.extend(["b", "c"]) (d) t.extend("bc") (e) t = t + ["b"]
Exercise 3 — find the nested list. A colleague's list of tests prints as ['smoke', ['reset', 'burst']] when they expected three test names. Say which method they used, which they wanted, and how len() would have revealed the problem sooner.
Exercise 4 — the one-character difference. Predict second in each case, then verify and explain the difference in two sentences:
first = [1, 2]
second = first
first += [3]
print(second)first = [1, 2]
second = first
first = first + [3]
print(second)Exercise 5 — repetition. Write the expression that builds a list of eight "-" characters, and the one that builds a list of five zeros. Then predict len([0] * 0) and say why it is not an error.
Exercise 6 — the grid trap. Run both of these, write down both results, and explain in three sentences why they differ:
g1 = [[0] * 2] * 2
g1[0][0] = 9
print(g1)g2 = [[0] * 2, [0] * 2]
g2[0][0] = 9
print(g2)Exercise 7 — choose the tool. For each, name +, extend or append and justify in one sentence:
(a) add one new failure to the failures list
(b) merge yesterday's failures into today's list, keeping yesterday's list intact
(c) add every name from a second test list into the working list
10. Summary
a + bbuilds a new list and leaves both originals unchanged. Both operands must be lists.a.extend(b)addsb's elements intoa, changingaand returningNone.bis untouched.append(x)addsxas one element;extend(xs)adds each element ofxs. Appending a list nests it;len()is how you notice.extendgiven a string adds its characters — not an error, and rarely what you meant.- The recurring choice:
+/sorted()/[:]build something new;extend/sort()change what you have. Ask whether anything still needs the list as it is. a += [x]mutates the list;a = a + [x]rebinds the name. On lists these are not interchangeable, and any other name referring to that list can tell the difference.a * nrepeats a list into a new list. A zero or negative count gives[], quietly.[[0] * 3] * 2does not make two rows. It makes one row referred to twice, so changing one changes both. Write the rows out instead.
Repeating a list of lists went wrong. The next chapter works with lists of lists deliberately.
Where this fits
Part of the Python curriculum.
