This lesson introduces the list as a sequence whose elements can be of any type, shows that everything learned about string indexing and slicing carries over unchanged, and then introduces the one difference that changes everything: a list can be modified in place.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 10 — Lists
§10.1-10.5, pp. 89-91
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §10.1-10.5, pp. 89-91 — the pages these objectives are drawn from
Warm-up
Strings have carried you through two chapters. Find their limit.
Discussion prompt
A string is a sequence of characters. Name two things you might want to keep in a sequence that a string cannot hold, and say what goes wrong if you try.
Hint: Think about numbers, and about things that are themselves sequences.
Answer:
Numbers, for one: '42' is text, and doing arithmetic on it needs a conversion every time.
And a sequence of words, or of other sequences. A string can hold the characters of several words, but not the words as separate items — you would have to remember where each one started.
A list solves both. Like a string, a list is a sequence of values; in a string the values are characters, and in a list they can be any type at all.
Concept
Like a string, a list is a sequence of values. Everything you learned about indexing, slicing, traversal and the in operator carries over unchanged — and then one thing is different, and it changes how you write nearly everything.
list — A sequence of values, which may be of any type, and which can be modified after it is created.
The values in a list are called elements, or sometimes items. Unlike strings, lists are mutable — and that single difference is the subject of the rest of the chapter.
Figure (svg): Two columns comparing strings and lists across the properties that matter
Think Python, 2nd edition — Allen B. Downey §10.1-10.5, pp. 89-90
Section
Section 1
Concept
There are several ways to create a new list; the simplest is to enclose the elements in square brackets. The elements do not have to be the same type, and one of them may be another list.
>>> [10, 20, 30, 40]
[10, 20, 30, 40]
>>> ['crunchy frog', 'ram bladder', 'lark vomit']
['crunchy frog', 'ram bladder', 'lark vomit']
>>> ['spam', 2.0, 5, [10, 20]]
['spam', 2.0, 5, [10, 20]]
>>> []
[]| Expression | What it contains | Note |
|---|---|---|
| [10, 20, 30, 40] | four integers | all the same type |
| ['spam', 2.0, 5, [10, 20]] | a str, a float, an int, and a list | mixed types |
| [] | no elements at all | the empty list |
A list within another list is nested. A list that contains no elements is called an empty list, and you create one with empty brackets.
Think Python, 2nd edition — Allen B. Downey §10.1-10.5, pp. 89-89
Picture it
Lists are drawn as boxes with the word list outside and the elements inside.
Figure (svg): A state diagram showing three names pointing at three list objects: one with three strings, one with two numbers, and one empty
This is the state diagram from lesson 2a with a new kind of thing on the right of the arrow — and in lesson 10c the fact that the arrow points at an OBJECT rather than a value becomes the whole story.
Worked example
Predict the length before advancing. The nesting is the trap.
>>> t = ['spam', 1, ['Brie', 'Roquefort', 'Pol le Veq'], [1, 2, 3]]
>>> len(t)
4| Element | Its index | Note |
|---|---|---|
| 'spam' | element 0 | a string |
| 1 | element 1 | an int |
| ['Brie', ...] | element 2 | a list — but ONE element |
| [1, 2, 3] | element 3 | another list, also one element |
Count the top-level items.
Why: Four things separated by commas at the outer level: a string, an integer, and two lists.
Notice what len counts.
Why: Although a list can contain another list, the nested list still counts as a single element. The length of this list is four.
Look inside if you need to.
Why: t[2] is the whole inner list, and t[2][0] is its first element — the bracket operator applied twice.
Figure (svg): The state of the program after each line of Worked example a nested list counts as one element, drawn as a ladder with one rung per traced line
4. A nested list is one element of the outer list, however many things it contains itself.
Verify: Count what a flattened version would give.
Why: Flattened, the same data would be eight items. That len reports four rather than eight is what makes nesting a structure rather than a convenience — the shape carries information, and chapter 12 relies on it heavily.
Prediction
Count the top-level elements.
Predict first
What is len(['a', ['b', 'c'], 'd'])?
Correct: 3 — a string, a nested list, and another string, counted as three top-level elements.
Why: The nested list counts as a single element however many things it contains. This is what makes nesting a way of building structure: the outer list has three slots, and one of them happens to hold a list. Chapter 12's sequences of sequences depend entirely on this.
Worked example
Four operations from chapter 8, applied to a list unchanged.
>>> cheeses = ['Cheddar', 'Edam', 'Gouda']
>>> cheeses[0]
'Cheddar'
>>> cheeses[-1]
'Gouda'
>>> len(cheeses)
3
>>> 'Edam' in cheeses
True| Expression | What it does | Note |
|---|---|---|
| cheeses[0] | indexing from zero | same as a string |
| cheeses[-1] | negative index from the end | same as a string |
| len(cheeses) | the number of elements | same as a string |
| 'Edam' in cheeses | membership | same as a string |
Index it.
Why: The syntax for accessing the elements of a list is the same as for accessing the characters of a string. Remember that the indices start at 0.
Use everything else you know.
Why: Any integer expression can be used as an index; an out-of-range index gives an IndexError; a negative index counts backward from the end.
Test membership.
Why: The in operator also works on lists — though note what it tests: whether a value is an ELEMENT, not whether it is a subsequence.
Figure (svg): A list of three cheese names drawn as boxes with positive indices above and negative indices below
All four behave exactly as they did for strings. Chapter 8's whole vocabulary transfers with nothing changed.
Verify: Check one place where in behaves differently.
Why: For strings, 'na' in 'banana' is True because it tests substrings. For lists, in tests membership of a single element — so ['a','b'] in ['a','b','c'] is False, because no element equals that list. Same operator, subtly different question, and worth confirming rather than assuming.
Trap
A student writes [1, 2] in [1, 2, 3] expecting True, by analogy with 'ab' in 'abc'.
Carry the string behaviour across unchanged
Why: Nearly everything else did, so it is a reasonable expectation.
For lists, in tests whether a value is an ELEMENT. No element of [1, 2, 3] is the list [1, 2], so the answer is False.
in asks about elements for lists and about substrings for strings.
Read it as is this value one of the items
Why: Which for [1, 2, 3] means: is it 1, or 2, or 3?
Test a nested list to see the difference
Why: [1, 2] in [[1, 2], 3] is True, because there the list IS an element.
This is the one place chapter 8's knowledge does not transfer verbatim, and it is worth meeting deliberately — the operator is the same and the question it asks is not.
Discrimination
Almost everything transfers. One thing does not.
Sort into buckets
For each operation, does it behave the same way on a list as on a string?
Faded example
The simplest way to create one.
Fill in the blanks
results = []
print(len(results)) # should print 0
Why: Empty brackets create a list with no elements, and its length is 0. This is the standard starting point for the accumulator pattern from lesson 8b — you begin with an empty list and add to it, exactly as a string accumulator begins with an empty string. Note that [] is a list and '' is a string, and mixing them up gives a TypeError as soon as you try to append.
Socratic
A list can hold a string, a number and another list at once. Ask whether that is a good idea.
Discussion prompt
Some languages require every element of a list to be the same type. Name one advantage of Python's permissiveness and one thing it makes harder.
Hint: Think about what you can and cannot assume when reading a list.
Answer:
The advantage is expressiveness: a row of a table can be a list holding a name, an age and a score, without inventing a type to describe it. Chapter 12's sequences of sequences use this constantly.
What it makes harder is reasoning. You cannot assume that every element supports the same operations, so a loop that adds up a list will fail on the one element that happens to be a string.
The practical consequence is that a mixed list is usually a STRUCTURE — position 0 means one thing and position 1 another — while a uniform list is a COLLECTION. Chapter 12 introduces a separate type, the tuple, that is better suited to the first case.
Section
Section 2
Concept
Unlike strings, lists are mutable. When the bracket operator appears on the left side of an assignment, it identifies the element of the list that will be assigned.
>>> numbers = [42, 123]
>>> numbers[1] = 5
>>> numbers
[42, 5]| Part | What it does | Note |
|---|---|---|
| numbers[1] | on the LEFT of the assignment | names an element |
| = 5 | the element is replaced | in the existing list |
| numbers | the same list object | with a different element |
The one-eth element of numbers, which used to be 123, is now 5. Compare this with lesson 8b, where exactly this syntax on a string raised a TypeError — the message there was that a str object does not support item assignment, and it was about the type rather than the syntax.
Think Python, 2nd edition — Allen B. Downey §10.1-10.5, pp. 90-90
Picture it
No new list was created. The existing one now holds something different.
Figure (svg): Two columns contrasting modifying a list element with rebuilding a string
That distinction — repointing a name versus changing an object — is the one lesson 10c is entirely about, and it is why the same syntax means such different things for the two types.
Worked example
One works and one raises. The difference is the type, not the syntax.
>>> s = 'abc'
>>> s[0] = 'x'
TypeError: 'str' object does not support item assignment
>>> t = ['a', 'b', 'c']
>>> t[0] = 'x'
>>> t
['x', 'b', 'c']| Line | Why | Result |
|---|---|---|
| s[0] = 'x' | strings are immutable | TypeError |
| t[0] = 'x' | lists are mutable | works |
| the difference | the type, not the syntax | same line, two outcomes |
Notice the syntax is identical.
Why: Both lines put the bracket operator on the left of an assignment. Nothing about the writing distinguishes them.
Notice the outcomes are not.
Why: The string raises a TypeError and the list quietly succeeds.
Read the error message again.
Why: It says a str object does not support item assignment — which was a hint, in lesson 8b, that some other type does.
Figure (svg): A panel showing the same assignment succeeding on a list and raising a TypeError on a string
The list assignment succeeds and the string assignment raises. This is the clearest possible demonstration that mutability is a property of the type rather than of the operation.
Verify: Check that no new list was created.
Why: The name t still refers to the same list object throughout — nothing was reassigned. Lesson 10c introduces the is operator, which makes that checkable rather than merely assertable, and it is the fact the whole aliasing section depends on.
Prediction
The syntax is identical for both types.
t = ['a', 'b', 'c']
t[1] = 'x'
print(t)| Line | What happens | Result |
|---|---|---|
| t[1] on the left | names an element | lists are mutable |
| = 'x' | replaces it | in place |
| print(t) | the same list, changed | ['a', 'x', 'c'] |
Predict first
What does this print?
Correct: ['a', 'x', 'c'] — the element at index 1 was replaced, in the existing list.
Why: The identical line on a string raises a TypeError, because strings are immutable. Here it succeeds and modifies the list in place: no new list was created and t was never reassigned. That distinction between changing an object and repointing a name is what lesson 10c is about.
Worked example
Chapter 8's technique becomes unnecessary. Compare the two.
# a string: rebuild from slices
s = 'abc'
s = s[:1] + 'x' + s[2:]
# a list: change the element
t = ['a', 'b', 'c']
t[1] = 'x'| Version | What it takes | Note |
|---|---|---|
| the string version | two slices and a concatenation | three operations |
| the list version | one assignment | one operation |
| what changed | the list can be modified | so nothing must be rebuilt |
Recall the string technique.
Why: Lesson 8b's answer to change one character was to build a new string from the parts you wanted to keep, because nothing else was possible.
Compare the list version.
Why: One assignment. No slicing, no concatenation, no reassignment of the name.
Notice what this changes about your code.
Why: The accumulator pattern is still useful, and it is no longer forced. A list can be built by creating it and then filling it in.
Figure (svg): The state of the program after each line of Worked example what mutability lets you stop doing, drawn as a ladder with one rung per traced line
One line instead of three, and no new object. Mutability removes the need for the rebuild-from-slices technique that immutability forced.
Verify: Ask what the string version costs for a long string.
Why: It copies every character that is not being changed, so modifying one character of a million-character string copies a million characters. A list assignment touches one element. That efficiency difference is real, and it is one reason the book suggests building text as a list and joining it at the end.
Trap
A student who learned that strings cannot be changed, so operations return new ones applies the same reasoning to lists.
Generalise from the type you learned it on
Why: The rule was stated as a fact about sequences, and lists are sequences.
It was a fact about STRINGS. List methods mostly modify in place and return None, so t = t.sort() throws away the list and leaves t holding None — which lesson 10b covers in earnest.
Immutability is a property of the type, and only strings have it here.
Ask of every operation: does this modify, or does it produce something new?
Why: For lists both kinds exist, which is what makes the question necessary.
Check by looking at the original afterwards
Why: If it changed, the operation modified. If it did not, the operation returned something you needed to keep.
This is the single most common source of confusion in the chapter, and the book devotes a whole debugging section to it — including a list of four ways to append that are all wrong.
Comparison
Fill the blanks. Only one row differs.
Comparison matrix
| Operation | String | List |
|---|---|---|
| s[0] | the first character | the first element |
| len(s) | the number of characters | the number of elements |
| s[0] = x | TypeError | replaces the element, in place |
| s[1:3] | a new string | a new list |
Three rows are the same idea with a different word. The third row is the difference the whole chapter turns on.
Faded example
The bracket operator, on the left.
Fill in the blanks
scores = [10, 20, 30]
scores[1] = 99 # should give [10, 99, 30]
Why: Index 1 is the second element, since indices start at zero. Putting the selection on the left of the assignment names the element to be replaced rather than producing its value — which is the same syntax that raises a TypeError on a string. Note that the list object is modified: no new list is created and scores is not reassigned.
Explain it to yourself
State it in terms of objects and names.
Discussion prompt
Explain what it means for a list to be mutable, using the words object and name — and say why reassigning a variable is not an example of mutation.
Hint: One of them changes what the arrow points at; the other changes what is at the end of it.
Answer:
Mutable means the OBJECT can be changed: the same list, after the operation, holds something different from what it held before.
Reassigning a name is different — it moves the arrow to point at a different object, and leaves the original object exactly as it was. That works for immutable types too, which is why strings can be reassigned freely.
The distinction matters because two names can point at one object. Changing the object is visible through both names; moving one name is visible through neither. That is the whole of lesson 10c, and it only arises for mutable types.
Section
Section 3
Concept
The most common way to traverse the elements of a list is with a for loop, and the syntax is the same as for strings. That works well if you only need to READ the elements — but updating them needs the indices.
for cheese in cheeses:
print(cheese)
for i in range(len(numbers)):
numbers[i] = numbers[i] * 2| Loop | What the variable holds | What it allows |
|---|---|---|
| for cheese in cheeses | the loop variable holds an element | read only |
| for i in range(len(numbers)) | the loop variable holds an index | read and write |
| numbers[i] = ... | assignment through the index | modifies the list |
len returns the number of elements in the list, and range produces the indices from 0 to n minus one. Each time through the loop, i gets the index of the next element — and the assignment uses i both to read the old value and to write the new one.
Think Python, 2nd edition — Allen B. Downey §10.1-10.5, pp. 91-91
Picture it
Assigning to the loop variable changes nothing. Assigning through an index changes the list.
Figure (svg): Two columns contrasting a for loop over elements with a loop over indices, showing which can modify the list
This is why range(len(t)) exists as an idiom. It is the standard way to get at the positions when the elements alone are not enough.
Worked example
The index is needed on both sides of the assignment.
numbers = [1, 2, 3]
for i in range(len(numbers)):
numbers[i] = numbers[i] * 2
print(numbers)| Pass | What happens | The list after |
|---|---|---|
| i = 0 | numbers[0] = 1 * 2 | [2, 2, 3] |
| i = 1 | numbers[1] = 2 * 2 | [2, 4, 3] |
| i = 2 | numbers[2] = 3 * 2 | [2, 4, 6] |
Get the indices with range and len.
Why: range returns the indices from 0 to n minus one, which are exactly the valid positions.
Read through the index.
Why: numbers[i] on the right of the assignment produces the current element.
Write through the same index.
Why: numbers[i] on the left names the element to be replaced. The assignment statement uses i to read the old value and to assign the new one.
Figure (svg): A list of three numbers shown before and after each element is doubled through its index
[2, 4, 6]. The list is modified in place, one element at a time, and no new list is created.
Verify: Try it with a plain for loop and watch nothing happen.
Why: for x in numbers: x = x * 2 leaves the list unchanged, because x is a name holding a copy of the element and assigning to it just repoints x. That the two loops look similar and behave completely differently is exactly why the index version is worth knowing.
Prediction
The assignment target is the loop variable.
t = [1, 2, 3]
for x in t:
x = x * 2
print(t)| Line | What happens | Result |
|---|---|---|
| x = x * 2 | repoints the name x | the list is not mentioned |
| each pass | x is reassigned from the list | the previous value is discarded |
| print(t) | unchanged | [1, 2, 3] |
Predict first
What does this print?
Correct: [1, 2, 3] — assigning to the loop variable repoints a name and leaves the list untouched.
Why: x is a name that holds each element in turn, and x = x * 2 makes x refer to a new number. The list is never mentioned on the left of any assignment, so nothing about it changes. The loop runs, does work, and has no effect — which is a silent failure with no error to indicate it. The index version is what modifies a list.
Worked example
A property of the for loop, and it is exactly what you want.
for x in []:
print('This never happens.')| Aspect | What is true | Note |
|---|---|---|
| the list | has no elements | nothing to iterate over |
| the body | never runs | zero passes |
| no error | an empty sequence is legal | traversing it does nothing |
Notice there is no special case.
Why: A for loop over an empty list never runs the body, without any check being needed.
Connect it to the while loop.
Why: This is the same property as lesson 7a's test-before-the-body rule, and lesson 8a's empty-string traversal, arriving a third time.
Notice what it saves.
Why: Code that processes a list needs no guard for the empty case, which is the commonest special case there is.
Figure (svg): The state of the program after each line of Worked example the empty list runs zero times, drawn as a ladder with one rung per traced line
Nothing is printed and no error occurs. Traversing an empty sequence doing nothing is the correct behaviour, and it comes for free.
Verify: Check that range(len([])) behaves the same way.
Why: len is 0, so range produces no indices and the loop body never runs. Both traversal forms handle the empty case correctly without a guard, which is worth confirming because the index version involves arithmetic that could plausibly have gone wrong.
Trap
A student writes for x in t: x = x * 2, expecting the list to be doubled.
Read the loop variable as the element itself
Why: It holds the element, so assigning to it looks like changing the element.
It is a name holding a reference to the element, and assigning to it repoints that name. The list is untouched, and the loop runs to completion with no error and no effect.
To change the list you need the position, not the value.
Loop over range(len(t)) when you need to write
Why: Then t[i] on the left of an assignment names an element of the actual list.
Use the plain for loop when you only need to read
Why: It is shorter and cannot go out of range.
The symptom is characteristic: a loop that obviously does something and a list that is unchanged afterwards. If you see that, check whether the assignment target is a loop variable or an indexed element.
Discrimination
Ask whether the body writes to the list.
Sort into buckets
For each task, is a plain for loop enough, or do you need indices?
Faded example
You need the positions, not the values.
Fill in the blanks
for i in range(len(t)):
t[i] = t[i] + 1
Why: range(len(t)) produces the indices 0 to n-1, which are exactly the valid positions. Each pass reads t[i] and writes back to the same position, modifying the list in place. Note that range(len(t)) handles the empty list correctly with no guard — len is 0, range produces nothing, and the body never runs.
Socratic
State it in terms of names and objects.
Discussion prompt
Explain why for x in t: x = 0 leaves the list unchanged, using what you know about names referring to values.
Hint: What exactly does the assignment change?
Answer:
x is a name, and the assignment repoints it at a different object. That is lesson 2a's rule, unchanged: an assignment moves an arrow.
The arrow being moved is x's, and the list holds its own references to its elements. Moving x's arrow tells the list nothing.
To change what the list holds you have to assign to a position IN the list, which is what t[i] = 0 does. The bracket on the left is what makes the difference, and it is the same syntax that raises a TypeError on an immutable type.
Section
Section 4
Concept
The plus operator concatenates lists and the star operator repeats one a given number of times — exactly as they did for strings in lesson 2b.
>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> a + b
[1, 2, 3, 4, 5, 6]
>>> [0] * 4
[0, 0, 0, 0]
>>> [1, 2, 3] * 3
[1, 2, 3, 1, 2, 3, 1, 2, 3]| Expression | What it does | Result |
|---|---|---|
| a + b | joins two lists end to end | a NEW list |
| [0] * 4 | repeats a one-element list | four zeros |
| [1, 2, 3] * 3 | repeats a three-element list | nine elements |
Both operators produce a NEW list and leave the originals unchanged — which makes them the immutable-style operations on a mutable type, and that distinction becomes important in lesson 10c.
Think Python, 2nd edition — Allen B. Downey §10.1-10.5, pp. 91-91
Picture it
This is worth noticing precisely because lists CAN be modified.
Figure (svg): Two columns contrasting operations that create a new list with those that modify an existing one
The right-hand column is the subject of lesson 10b. The important thing here is that the operators are on the left — a + b does not change a.
Worked example
The commonest use of the star operator, and it has a hazard.
>>> counts = [0] * 5
>>> counts
[0, 0, 0, 0, 0]
>>> counts[2] = 7
>>> counts
[0, 0, 7, 0, 0]| Line | What happens | Note |
|---|---|---|
| [0] * 5 | five copies of the number zero | a list of length 5 |
| counts[2] = 7 | modify one position | the others are unaffected |
| why safe | numbers are immutable | each element is independent |
Create a list of a known length.
Why: Repetition is how you make a list of a given size before you know what will go in it, which the counter patterns of chapter 13 use constantly.
Modify one position.
Why: The other elements are unaffected, so this behaves exactly as you would expect.
Notice why it is safe here.
Why: The repeated element is a number, which is immutable — so it does not matter that all five positions initially refer to the same zero.
Figure (svg): The state of the program after each line of Worked example repetition builds a list of a known size, drawn as a ladder with one rung per traced line
A list of five zeros, of which one can be changed independently. Repetition is the standard way to create a list of a known size.
Verify: Ask what would happen if the repeated element were a list.
Why: [[0]] * 3 gives three references to the SAME inner list, so modifying one modifies all three. That is an aliasing bug and it is genuinely surprising — it is worth knowing the hazard exists, and lesson 10c explains exactly why it happens.
Prediction
The plus operator creates rather than modifies.
a = [1, 2]
b = [3]
a + b
print(a)| Line | What happens | Result |
|---|---|---|
| a + b | creates a new list | [1, 2, 3] |
| no assignment | the new list is discarded | nothing kept |
| print(a) | unchanged | [1, 2] |
Predict first
What does this print?
Correct: [1, 2] — the concatenation created a new list which was immediately discarded, and a was never modified.
Why: This is the wasted-return-value mistake from lesson 6a, on a new type. The plus operator produces a new list and does not touch its operands, so a line consisting of a + b alone computes something and throws it away. Note that no error occurs, which is what makes it worth meeting deliberately.
Worked example
A slice on the left of an assignment can replace several elements at once.
>>> t = ['a', 'b', 'c', 'd', 'e', 'f']
>>> t[1:3] = ['x', 'y']
>>> t
['a', 'x', 'y', 'd', 'e', 'f']| Part | What it does | Note |
|---|---|---|
| t[1:3] | on the LEFT of the assignment | names two elements |
| = ['x', 'y'] | replaces them | in place |
| the result | the same list, two elements changed | no new list |
Recognise the slice, from lesson 8b.
Why: t[1:3] selects the elements at indices 1 and 2, half-open as always.
Notice it is on the left.
Why: A slice operator on the left side of an assignment can update multiple elements — which for a string was impossible.
Check what changed.
Why: Two elements replaced, in the existing list. Nothing was created and no name was reassigned.
Figure (svg): A six-element list with the two elements at indices one and two highlighted as the ones being replaced
['a', 'x', 'y', 'd', 'e', 'f']. Slice assignment replaces a run of elements in place, which is the multi-element version of t[i] = x.
Verify: Try replacing a slice with a different number of elements.
Why: t[1:3] = ['x'] leaves a shorter list, and t[1:3] = ['x','y','z'] a longer one — the replacement need not match the slice's length. That flexibility is genuinely useful and worth discovering deliberately, since it means slice assignment can insert and delete as well as replace.
Trap
A student writes t + [x] on a line by itself, expecting it to add an element to t.
Assume that because lists are mutable, operations on them modify
Why: The type can be modified, so an operation on it plausibly does.
The plus operator creates a new list and returns it. On a line by itself the result is discarded and t is unchanged — a legal statement with no effect at all.
Operators create; methods mostly modify.
If you use +, assign the result
Why: t = t + [x], which repoints t at the new list.
If you want to modify in place, use append
Why: t.append(x), which changes the existing list and returns None.
The book lists exactly this among four wrong ways to add an element, and notes that only one of them causes a runtime error — the other three are legal and do the wrong thing. Lesson 10b works through all four.
Sorting
Both kinds exist for lists, which is what makes the question necessary.
Sort into buckets
For each operation on a list t, does it create or modify?
Faded example
The repetition operator, on a one-element list.
Fill in the blanks
counts = [0] ***** 10
print(len(counts)) # should print 10
Why: Repetition takes a list and an integer and produces a new list with the elements repeated. Note the shape of the left operand: [0] * 10 gives ten zeros, while 0 * 10 is just the number 0. Repeating a list containing a MUTABLE element is a different matter — [[0]] * 3 gives three references to one inner list, which is an aliasing hazard lesson 10c explains.
Explain it
For strings it was a curiosity. Here it has a purpose.
Discussion prompt
For a string, s[:] gives back the same string and seems pointless. For a list it is genuinely useful. Explain why, without using the word aliasing.
Hint: What can you do to a list that you cannot do to a string?
Answer:
Say: a list can be modified, so sometimes you want a separate copy that you can change without affecting the original. t[:] gives you one.
For a string it makes no difference, because nothing can change either copy — which is why it looked pointless there.
The book puts it directly: since lists are mutable, it is often useful to make a copy before performing operations that modify lists. That is why the same expression is a curiosity for one type and a technique for the other.
Section
Section 5
Concept
Everything in this lesson comes down to two questions: does this operation read or write, and does it create a new list or modify an existing one? The answers determine which loop you need and whether the result must be assigned.
# reads only, creates nothing:
for x in t:
print(x)
# writes into the existing list:
for i in range(len(t)):
t[i] = t[i] * 2
# creates a new list, leaves t alone:
doubled = [x * 2 for x in t] # chapter 19; for now, a loop| Intent | Which form | Effect on t |
|---|---|---|
| read only | a plain for loop | the simplest form |
| write in place | indices, and assignment to t[i] | modifies t |
| build a new one | an accumulator | t is unchanged |
The third form is the accumulator pattern from lesson 8b, which is still available and no longer compulsory. Choosing between the second and third is a real decision, and lesson 10c gives the reason it matters.
Think Python, 2nd edition — Allen B. Downey §10.1-10.5, pp. 90-91
Picture it
The intent decides the shape, and the shape decides what the caller sees.
Figure (svg): A three-stage diagram showing read-only, modify-in-place and build-new as three distinct loop shapes
Getting this choice wrong is the commonest bug in the chapter, and its symptom is always the same: a loop that clearly does something and a list that is unchanged.
Worked example
Modify in place, or build a new list. Both are correct and they differ.
# in place
def double_all(t):
for i in range(len(t)):
t[i] = t[i] * 2
# building a new list
def doubled(t):
res = []
for x in t:
res.append(x * 2)
return res| Function | What it does | Note |
|---|---|---|
| double_all | modifies the caller's list | returns None |
| doubled | leaves the caller's list alone | returns a new list |
| the choice | who should see the change? | a design decision |
Write the in-place version.
Why: It needs indices, and it returns nothing — its whole effect is on the list it was given.
Write the new-list version.
Why: It uses the accumulator pattern from lesson 8b, reads with a plain for loop, and returns a result.
Notice that both are correct.
Why: They are different functions with different contracts, and neither is a broken version of the other.
Figure (svg): Two columns comparing an in-place function with one that returns a new list, on what each does and how each is misused
Two functions, one modifying and one returning. The choice is about who should see the change, and it must be documented — a caller cannot tell from the name.
Verify: Check what each returns.
Why: double_all returns None, because it has no return statement — so t = double_all(t) would destroy the list. doubled returns the new list, so doubled(t) on its own would discard it. Each has a characteristic misuse, and they are opposites.
Prediction
The function modifies through an index.
def zero_first(t):
t[0] = 0
nums = [7, 8, 9]
zero_first(nums)
print(nums)| Line | What happens | Result |
|---|---|---|
| t[0] = 0 | assigns to a position | in the list itself |
| the caller's list | the same object | shared |
| print(nums) | changed | [0, 8, 9] |
Predict first
What does this print?
Correct: [0, 8, 9] — the function modified the list, and the caller sees the change.
Why: The parameter t and the variable nums refer to the same list object, so an assignment to t[0] changes what nums refers to as well. This is genuinely new: no function so far could affect a caller's variable, because every earlier type was immutable. Lesson 10c makes the mechanism explicit with a stack diagram.
Worked example
The symptom is specific enough to name the cause.
def double_all(t):
for x in t:
x = x * 2
nums = [1, 2, 3]
double_all(nums)
print(nums)| Part | What happens | Result |
|---|---|---|
| the loop | runs three times | does arithmetic |
| x = x * 2 | repoints a local name | the list is not mentioned |
| print(nums) | unchanged | [1, 2, 3] |
Confirm the loop runs.
Why: It does, three times, and computes three products. Nothing is wrong with the arithmetic.
Find what the assignment targets.
Why: x, which is a local name. Assigning to it repoints that name and tells the list nothing.
State the fix.
Why: Loop over range(len(t)) and assign to t[i], so that the assignment names a position in the actual list.
Figure (svg): The state of the program after each line of Worked example diagnosing a loop that does nothing, drawn as a ladder with one rung per traced line
The list is unchanged. The assignment repoints a loop variable rather than replacing an element, which is a silent failure with no error at all.
Verify: Add a print inside the loop and confirm the work is happening.
Why: Printing x after the multiplication shows 2, 4 and 6, so the computation is correct and only the storing is wrong. Separating is the work happening from is it being kept is exactly lesson 6c's three-possibilities method, applied here.
Trap
A function sorts the list it is given, and the caller is surprised that their original list has been reordered.
Modify in place because it is convenient
Why: It avoids building a copy and the function is shorter.
The caller's list changed without the call site saying so. sort_it(t) looks like it produces something, and instead it silently reorders data the caller may still need.
Decide deliberately, and make the choice visible.
Name in-place functions with a verb that implies change
Why: sort_in_place, or the convention of returning None so that misuse fails.
Otherwise build and return a new list
Why: Which leaves the caller's data alone and makes the function usable in an expression.
This is lesson 4b's contract again: modifying an argument is a postcondition the caller has to know about, and it belongs in the docstring. Python's own library makes the same distinction — sort modifies and sorted returns a new list.
Error analysis
One works. Mark what is wrong with the other two.
Annotate
The third is a reminder that the star operator repeats a list rather than scaling its elements, which is the same distinction as for strings.
Comparison
Fill the blanks. The intent decides the form.
Comparison matrix
| Intent | Loop form | Effect on the original |
|---|---|---|
| read each element | for x in t | unchanged |
| update each element | for i in range(len(t)) | modified in place |
| build a related list | for x in t, appending to a new list | unchanged |
Two of the three leave the original alone, and only one of those produces anything — which is why the third form must return its result.
Real world
The decision is not specific to Python.
Discussion prompt
Think of a tool or service that either edits something in place or produces a new version — a document editor, an image filter, a file operation. What are the consequences of each choice, and when is in place the wrong default?
Hint: Consider whether the original is still needed.
Answer:
Editing in place is efficient and destructive: the original is gone. Producing a new version costs space and keeps both, which is why every serious editor has undo.
In place is the wrong default whenever the original might still be wanted, or whenever somebody else might be looking at it — which for a shared list is exactly the case.
Python's library makes the choice explicit in its naming: sort modifies and sorted returns a new list, list.reverse modifies and reversed produces a new sequence. Following that convention in your own functions is the clearest way to make the contract visible.
Comparison
Fill the blanks. The last row is the difference everything else follows from.
Comparison matrix
| Property | String | List |
|---|---|---|
| what the elements are | characters | values of any type |
| indexing and slicing | from 0, half-open slices | identical |
| what in tests | a substring | membership of a single element |
| can it be modified? | no — immutable | yes — mutable |
Two rows the same, two different. The third catches people once; the fourth changes how you write everything.
Pattern
Five steps, and the first two decide the shape before any code is written.
Step 3 is the one whose failure is silent. A loop that assigns to its loop variable runs correctly, does the arithmetic, and has no effect at all.
Python documentation — An Informal Introduction to Python An Informal Introduction to Python
Check
The same line, on two types.
t = ['a', 'b', 'c']
t[0] = 'x'| Part | What happens | Note |
|---|---|---|
| t[0] on the left | names an element | lists are mutable |
| the assignment | replaces it in place | no new list |
| on a string | the same line raises | TypeError |
Check your understanding
Why does t[0] = 'x' work for a list but not for a string?
Answer: B
Why: The syntax is identical; the difference is entirely in the type. A list can be modified after it is created, so an element can be replaced in place. A string cannot, which is why the same line raises a TypeError saying that a str object does not support item assignment.
Check
The assignment target decides whether anything changes.
t = [1, 2, 3]
for x in t:
x = 0
print(t)| Line | What happens | Result |
|---|---|---|
| x = 0 | repoints the loop variable | a local name |
| the list | never appears on the left | untouched |
| print(t) | unchanged | [1, 2, 3] |
Check your understanding
What does this print?
Answer: B
Why: Assigning to the loop variable repoints a local name and leaves the list untouched — the list never appears on the left of any assignment. To modify the list you need the positions: for i in range(len(t)) with t[i] = 0. The failure is silent, which is what makes it worth meeting deliberately.
Check
Operators create; indexed assignment modifies.
Check your understanding
Which of these modifies the existing list t rather than creating a new one?
Answer: C
Why: A slice on the LEFT of an assignment names part of the existing list and replaces it in place. The other three are expressions that produce new lists and leave t alone — and a result that is not assigned is simply discarded.
Real world
Editing in place versus producing a new version is a distinction with everyday consequences.
Discussion prompt
Think of two tools that do the same job, one editing a file in place and one writing a new file. What does each get right, and which mistakes does each make possible?
Hint: Consider what happens when something goes wrong halfway through.
Answer:
In-place editing is fast and needs no extra space, and it destroys the original — so a failure halfway through leaves you with neither the old version nor a complete new one.
Writing a new file is safe and slower, and it leaves you to decide what to do with two versions.
The programming version is exactly the same trade, and Python's library makes the choice visible in its names: sort modifies and sorted returns a new list. Adopting that convention in your own functions is the clearest way to tell a caller which kind they are getting.
Commit first
Answer, then rate your confidence. This is the chapter's most common silent bug.
Predict first
A function contains for x in t: x = x * 2. What happens to the caller's list?
Correct: Nothing — the list is unchanged, because assigning to the loop variable repoints a local name rather than replacing an element.
Why: x is a name that holds each element in turn, and x = x * 2 makes x refer to a different number. The list is never mentioned on the left of any assignment, so nothing about it changes. The loop runs, performs the arithmetic correctly, and has no effect — with no error to indicate it. The fix is to loop over range(len(t)) so that t[i] on the left names a position in the actual list. This failure is worth over-learning because its symptom is so distinctive: a loop that obviously does work, and data that is unchanged afterwards.
Explain it
The mutability distinction is what a student coming from chapter 8 most needs.
Discussion prompt
A classmate has learned that string operations always return new strings and never modify. Explain what changes for lists, and give them one line that behaves differently on the two types.
Hint: The line is the same on both types.
Answer:
Say: strings are immutable, so every operation on one produces a new string. Lists are mutable, so some operations modify the list in place — and both kinds exist, which is what makes it necessary to know which is which.
The line: t[0] = 'x'. On a list it replaces the first element; on a string it raises a TypeError saying that a str object does not support item assignment.
Then give them the question to ask about every list operation from now on: does this create something new, or change what I already have? For strings the answer was always the first; for lists it has to be checked.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: Creating lists is syntax and settles immediately, except for the nesting rule about length. Mutability is a single fact with very large consequences, and it is worth stating out loud until it is automatic. The loop choice is where the silent bug lives — a loop that assigns to its loop variable is the commonest mistake in this chapter. And the create-versus-modify distinction is the one that keeps mattering: the next lesson opens with four wrong ways to add an element to a list, three of which produce no error at all.
Connect it up
One page, from memory.
Draw it
Draw a list of four elements as a box with the word list beside it and the indices marked, with a name pointing at it. Then, in two columns, list every operation from this lesson that CREATES a new list and every one that MODIFIES the existing one. Finally, write the two loop forms side by side and mark which of them can change the list, with one sentence saying why the other cannot.
Recap
Three pages, and a sequence that can be changed.
| If you remember one thing | It is this |
|---|---|
| From lists as sequences | Everything from chapter 8 transfers, except what in tests. |
| From mutability | The bracket operator on the left is the difference between the two types. |
| From traversal | Assigning to a loop variable changes nothing. Use indices to write. |
| From the operators | Plus and star create new lists. They do not modify their operands. |
| From nesting | A nested list is one element, whatever it contains. |
The next lesson meets the list methods — append, extend, sort — and the fact that most of them modify the list and return None, which is the source of the single most common mistake in the chapter.
Think Python, 2nd edition — Allen B. Downey §10.1-10.5, pp. 89-91 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.