This lesson introduces the list methods and the fact that most of them return None, names the three patterns that most list processing is built from, gives four ways to delete an element, and converts between lists and strings with list, split and join.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 10 — Lists
§10.6-10.9, pp. 92-94
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §10.6-10.9, pp. 92-94 — the pages these objectives are drawn from
Warm-up
You have two tools already. Try both and notice what each leaves behind.
Discussion prompt
Using only the previous lesson, write two different ways to add the number 4 to the end of a list t. What is different about what each one leaves you with?
Hint: One of them uses an operator.
Answer:
t = t + [4] builds a new list and repoints t at it. The original list still exists, unchanged, and t no longer refers to it.
There is no second way yet — the previous lesson had no method for adding an element. This lesson supplies one, and it works quite differently.
append modifies the existing list rather than creating a new one, and the difference between those two matters enormously once more than one name refers to the same list. That is lesson 10c.
Concept
Python provides methods that operate on lists, and most of them are void: they modify the list and return None. If you accidentally write t = t.sort(), you will be disappointed with the result.
void method — A method that modifies the object it is called on and returns None.
This is the exact opposite of string methods, which never modify and always return a new string. The two types behave in opposite ways, and applying either habit to the other type produces a bug with no error message.
Figure (svg): Two columns contrasting string methods with list methods on what they modify and what they return
Think Python, 2nd edition — Allen B. Downey §10.6-10.9, pp. 92-92
Section
Section 1
Concept
append adds a new element to the end of a list, extend appends all the elements of another list, and sort arranges the elements from low to high. All three modify the list and return None.
>>> t = ['a', 'b', 'c']
>>> t.append('d')
>>> t
['a', 'b', 'c', 'd']
>>> t1 = ['a', 'b', 'c']
>>> t2 = ['d', 'e']
>>> t1.extend(t2)
>>> t1
['a', 'b', 'c', 'd', 'e']| Call | What it does | Note |
|---|---|---|
| append('d') | adds ONE element | the list grows by one |
| extend(t2) | adds all of t2's elements | the list grows by len(t2) |
| t2 afterwards | unmodified | extend reads it and does not change it |
Notice that neither call was assigned to anything. That is correct: the method modifies the list, so there is nothing to keep. Assigning the result would store None.
Think Python, 2nd edition — Allen B. Downey §10.6-10.9, pp. 92-92
Picture it
The difference is whether the argument is added as an element or unpacked.
Figure (svg): Two columns contrasting append adding its argument as one element with extend adding each of its argument's elements
Using append where extend was meant gives a nested list — which the book lists among the four wrong ways to add an element, because it is legal and does the wrong thing.
Worked example
The single most common mistake in the chapter. Cause it deliberately.
>>> t = ['d', 'c', 'a']
>>> t = t.sort()
>>> t
None
>>> len(t)
TypeError: object of type 'NoneType' has no len()| Step | What happens | Consequence |
|---|---|---|
| t.sort() | sorts the list in place | the list is now sorted |
| it returns None | because it is a void method | there is no result |
| t = None | the name is repointed at None | the sorted list is lost |
Notice that the sort itself worked.
Why: The list really was sorted, in place, before anything else happened.
Notice what the method returned.
Why: None, because most list methods are void: they modify the list and return None.
See what the assignment did.
Why: It repointed t at None, so the name no longer refers to the sorted list. The list still exists and nothing refers to it.
Figure (svg): A state diagram showing t repointed at None while the sorted list remains with nothing referring to it
t holds None and the sorted list is unreachable. Because sort returns None, the next operation you perform with t is likely to fail — which is at least loud.
Verify: Keep a second reference and confirm the list really was sorted.
Why: With u = t before the mistaken assignment, u still refers to the sorted list and shows ['a','c','d']. The sorting worked perfectly; only the assignment was wrong, and separating those two facts is what makes the diagnosis clear.
Prediction
The method modifies and returns None.
t = [3, 1, 2]
t = t.sort()
print(t)| Line | What happens | Result |
|---|---|---|
| t.sort() | sorts in place, returns None | the list is sorted |
| t = None | the name is repointed | the list is lost |
| print(t) | None | not the sorted list |
Predict first
What does this print?
Correct: None — sort modifies the list in place and returns None, which the assignment then stored in t.
Why: The sorting worked. The assignment is what went wrong: it repointed t at the method's return value, which is None, and the sorted list became unreachable. The correct usage is t.sort() with no assignment, or t = sorted(t) using the function that returns a new list.
Worked example
The book lists these deliberately. Only one of them raises an error.
# correct:
t.append(x)
t = t + [x]
t += [x]
# wrong:
t.append([x]) # adds a nested list
t = t.append(x) # t becomes None
t + [x] # result discarded
t = t + x # TypeError| Line | What happens | Note |
|---|---|---|
| t.append([x]) | adds one element, a list | legal, wrong shape |
| t = t.append(x) | append returns None | legal, t is destroyed |
| t + [x] | creates a list and discards it | legal, no effect |
| t = t + x | cannot concatenate a list and a non-list | TypeError |
Notice how many are legal.
Why: Three of the four wrong versions run without any error at all. Only the last one causes a runtime error.
Identify each failure.
Why: One adds a nested list, one replaces t with None, and one computes a new list and throws it away.
Draw the conclusion.
Why: Legality is not correctness. Three of these would survive a test that only checked for errors.
Figure (svg): The state of the program after each line of Worked example the four wrong ways to append, drawn as a ladder with one rung per traced line
Three wrong versions run silently and one raises. The book's own advice follows: try each in interactive mode to make sure you understand what they do.
Verify: Check the length after each of the three legal wrong versions.
Why: The nested-append gives a list one longer with a list inside it, the None assignment gives a TypeError on the next len, and the discarded concatenation leaves the length unchanged. Three different symptoms for three different mistakes, and none of them is an error at the point of the mistake.
Trap
A student who learned s = s.upper() writes t = t.sort() by analogy.
Generalise the assignment habit across types
Why: It was correct and necessary for every string method, so it looks like the rule for methods.
It was a rule about immutable types. List methods mostly modify in place and return None, so the assignment stores None and loses the list.
Ask of each method: does it modify, or does it return?
For a method that modifies, call it and do not assign
Why: t.sort() on its own is the correct usage.
For a function that returns, assign the result
Why: sorted(t) returns a new sorted list and leaves t alone — which is the function form of the same operation.
Python's library makes the distinction visible where it can: sort modifies and sorted returns, reverse modifies and reversed returns. Where two names exist, the shorter verb usually modifies.
Discrimination
Ask whether the argument should become one element or several.
Sort into buckets
For each intention, which method is right?
Sorting
The book gives three of each. Three of the wrong ones are legal.
Sort into buckets
For a list t and an element x, sort each line.
Explain it to yourself
It is a deliberate design decision. Argue for it.
Discussion prompt
Python's list methods could have returned the modified list, allowing t = t.sort() to work. Suggest why returning None is arguably better.
Hint: Consider what returning the list would suggest about what happened.
Answer:
Returning the list would suggest that a new list was produced — the shape of the call would look identical to sorted(t), which really does return a new list.
Returning None makes the difference visible: a method that gives you nothing back has clearly done its work somewhere else, namely in place.
It also fails loudly. t = t.sort() leaves t holding None, and the next operation raises — whereas if sort returned the list, code that modified a shared list while believing it had a private copy would fail silently and much later. Given that only one of the two can be loud, this is the better choice.
Section
Section 2
Concept
To add up all the numbers in a list you use an accumulator: a variable initialized before the loop and updated on every pass. An operation that combines a sequence of elements into a single value is sometimes called a reduce.
accumulator — A variable used in a loop to add up or accumulate a result.
def add_all(t):
total = 0
for x in t:
total += x
return total| Line | Its role | Note |
|---|---|---|
| total = 0 | the accumulator, initialized | before the loop |
| total += x | augmented assignment | same as total = total + x |
| return total | after the loop | one value from many |
The += operator provides a short way to update a variable: total += x is equivalent to total = total + x. Adding up a list is common enough that Python provides it as the built-in function sum.
Think Python, 2nd edition — Allen B. Downey §10.6-10.9, pp. 93-93
Picture it
That shape is what makes it a reduce.
Figure (svg): A diagram showing a list of several elements feeding an accumulator loop and producing a single value
You have written this before — the counter in lesson 8b and the string accumulator in lesson 8b are both reduces, one producing a number and one a string.
Worked example
A shorthand you will see constantly. Confirm it means what it says.
>>> total = 0
>>> total += 5
>>> total
5
>>> total = total + 5
>>> total
10| Line | What happens | Result |
|---|---|---|
| total += 5 | read total, add 5, store back | 5 |
| total = total + 5 | exactly the same thing | 10 |
| equivalence | the two forms are interchangeable | for numbers |
Read the shorthand.
Why: total += x is equivalent to total = total + x — read the old value, combine, and store back.
Notice it is an update.
Why: Lesson 7a's rule applies: the variable must be initialized first, or the read fails with a NameError.
Note the other forms.
Why: The same shorthand exists for the other operators: -=, *=, /= and so on, each meaning the same read-combine-store.
Figure (svg): The state of the program after each line of Worked example the augmented assignment, drawn as a ladder with one rung per traced line
The two forms are equivalent for numbers. += is shorter and says update more directly, which is why accumulator loops are usually written with it.
Verify: Try += without initializing first.
Why: It raises a NameError, exactly as total = total + x would, because both read the old value before assigning. The shorthand changes nothing about the semantics — which is worth confirming, since a shorter form can look like it does something different.
Prediction
The accumulator is initialized to zero.
def product(t):
result = 0
for x in t:
result *= x
return result| Step | What happens | Value |
|---|---|---|
| result = 0 | the wrong identity for multiplication | 0 |
| 0 * anything | still zero | 0 |
| every pass | leaves it at zero | 0 |
Predict first
What does product([2, 3, 4]) return?
Correct: 0 — multiplying the initial zero by anything leaves zero, so the answer is always zero whatever the list.
Why: The loop runs correctly and every multiplication is performed; the initialization destroys the result. The correct initial value for a multiplicative accumulator is 1, because one times anything is unchanged. The general rule is that the accumulator starts at the identity element for the operation — which is also the answer a reduce over an empty list should give.
Worked example
Python provides the commonest reduce. The others you write yourself.
>>> t = [1, 2, 3]
>>> sum(t)
6
# a reduce Python does not provide:
def product(t):
result = 1
for x in t:
result *= x
return result| Operation | Where it comes from | Note |
|---|---|---|
| sum(t) | the built-in reduce for addition | 6 |
| product | the same pattern with multiplication | written by hand |
| result = 1 | the identity for multiplication | not 0 |
Use the built-in where one exists.
Why: Adding up the elements of a list is such a common operation that Python provides it as sum.
Write your own where none does.
Why: The pattern is identical — initialize, loop, combine, return — with a different operator.
Notice the initialization changes.
Why: For addition the accumulator starts at 0; for multiplication it starts at 1, because that is the value that leaves the result unchanged.
Figure (svg): A ladder showing an accumulator growing from zero through partial sums to the total
sum for addition, and the same pattern by hand for anything else. The only thing that varies is the operator and the starting value that goes with it.
Verify: Check both on the empty list.
Why: sum([]) is 0 and product([]) is 1 — each returns its accumulator's initial value. That those are the right answers is not a coincidence: the identity element is exactly the value that a reduce over nothing should produce, which is why it is the right initialization.
Trap
A student writes a product function and initializes the accumulator to 0.
Copy the initialization from the summing version
Why: The pattern is the same, so the starting value looks like part of the boilerplate.
Every product comes out as 0, because multiplying by the initial zero destroys everything. The loop runs correctly and the answer is always the same.
The initial value is the one that leaves the result unchanged.
For addition, zero; for multiplication, one
Why: Adding zero and multiplying by one both change nothing, which is what an empty accumulator should do.
Check it against the empty list
Why: A reduce over no elements should return the initialization, and that answer should be sensible.
For a string accumulator the same reasoning gives the empty string, and for a list accumulator the empty list. In every case the question is: what value combined with anything leaves it alone?
Faded example
A reduce that counts rather than sums.
Fill in the blanks
def count_big(t):
n = 0
for x in t:
if x > 10:
n += 1
return n
Why: A counting accumulator starts at zero, because zero is the right answer for a list with no big elements — including the empty list. Note the shape: this is the counter pattern from lesson 8b, now recognised as a kind of reduce. What makes it a reduce is that many values go in and one comes out.
Discrimination
Ask what shape the output has.
Sort into buckets
For each operation on a list, is it a reduce?
Socratic
It is not a coincidence that it also answers the empty case.
Discussion prompt
For a summing accumulator the initial value is 0 and for a product it is 1. Explain why the identity element is the right choice, using the empty list as your argument.
Hint: What should a reduce over nothing produce?
Answer:
A reduce over an empty list returns its initialization untouched, so the initialization IS the answer for the empty case — and the sensible answer there is the identity.
It also has to be the identity for non-empty lists, because the first pass combines it with the first element and must leave that element unchanged.
So one requirement settles both cases at once, which is a good sign that it is the right way to think about it rather than two separate rules to remember.
Section
Section 3
Concept
Sometimes you want to traverse one list while building another. An operation that applies something to every element is called a map; one that selects some of the elements is called a filter.
def capitalize_all(t):
res = []
for s in t:
res.append(s.capitalize())
return res
def only_upper(t):
res = []
for s in t:
if s.isupper():
res.append(s)
return res| Function | Relationship of input to output | Which pattern |
|---|---|---|
| capitalize_all | one output per input | a map |
| only_upper | some inputs produce output | a filter |
| both | res is an accumulator holding a list | the same shape |
res is initialized with an empty list, and each time through the loop the next element is appended — so res is another kind of accumulator. Most common list operations can be expressed as a combination of map, filter and reduce.
Think Python, 2nd edition — Allen B. Downey §10.6-10.9, pp. 93-94
Picture it
The length of the result is what tells them apart.
Figure (svg): Two columns comparing map, filter and reduce by the length of what each produces
Naming them matters because most list processing is a combination of the three, and because chapter 19 provides a compact syntax for map and filter.
Worked example
One output element per input element, always.
>>> t = ['alice', 'bob', 'carol']
>>> capitalize_all(t)
['Alice', 'Bob', 'Carol']
>>> len(t), len(capitalize_all(t))
(3, 3)| Aspect | What happens | Note |
|---|---|---|
| each element | transformed by capitalize | one output each |
| the length | unchanged | 3 in, 3 out |
| the original | untouched | a new list was built |
Notice the unconditional append.
Why: Every element produces exactly one output, because the append is not inside an if.
Check the length.
Why: A map always produces a list the same length as its input. That is the defining property.
Notice the original is unchanged.
Why: capitalize is a string method, which returns a new string, and the results go into a new list. Nothing modifies t.
Figure (svg): A diagram showing three elements each transformed independently into three output elements
Three names in, three capitalized names out. A map maps a function onto each element, and the length is preserved.
Verify: Check the length property on the empty list.
Why: capitalize_all([]) gives [] — length zero in, length zero out. The property holds at the boundary, which is a quick confirmation that the loop has no special cases hiding in it.
Prediction
The append is inside the condition.
def positives(t):
res = []
for x in t:
if x > 0:
res.append(x)
return res| Step | What happens | Note |
|---|---|---|
| input [1, -2, 3] | one fails the test | skipped |
| the appends | two happen | two elements |
| the result | shorter than the input | a filter |
Predict first
What is the length of positives([1, -2, 3, -4])?
Correct: 2 — only the two positive elements are appended, so the output is shorter than the input.
Why: The append is inside the if, so an element that fails the test produces nothing at all. That is what makes this a filter rather than a map: the output length depends on the data, whereas a map's output is always the same length as its input.
Worked example
The append is inside a condition, so some elements produce nothing.
>>> t = ['ABC', 'def', 'GHI']
>>> only_upper(t)
['ABC', 'GHI']
>>> len(t), len(only_upper(t))
(3, 2)| Element | The test | Result |
|---|---|---|
| 'ABC' | isupper is True | appended |
| 'def' | isupper is False | skipped |
| 'GHI' | isupper is True | appended |
Notice the conditional append.
Why: The append is inside an if, so an element only reaches the output if it passes the test.
Check the length.
Why: Two out of three, so the output is shorter. A filter selects some of the elements and filters out the others.
Notice the elements are unchanged.
Why: A filter selects; it does not transform. The strings that survive are exactly the ones that went in.
Figure (svg): The state of the program after each line of Worked example a filter can shorten, drawn as a ladder with one rung per traced line
Two elements out of three. A filter's output is a sublist of its input — same elements, possibly fewer of them.
Verify: Check the two extremes.
Why: A filter whose test is always true returns a list the same length as the input, and one whose test is always false returns an empty list. If a filter can never produce either extreme, its condition is not actually filtering — which is the same check as lesson 8b's counter that always equals the length.
Trap
A student wants only the positive numbers and writes a loop that appends every element, with an if that only decides what to append.
Put the condition in the wrong place
Why: The if is present, so the filtering feels done.
If the append happens on both branches, the output has one element per input and is a map. The condition changed the values rather than the selection.
A filter puts the append INSIDE the condition.
Ask what happens to an element that fails the test
Why: In a filter, nothing at all — no append, no output.
Check the length of the result
Why: A filter can produce a shorter list; a map never can.
The length is the reliable test. If your filter always returns a list the same length as its input, the append has escaped from the condition — which is the same structural mistake as a return escaping from an if in lesson 6a.
Sorting
The shape of the output decides.
Sort into buckets
For each operation on a list, which pattern is it?
Faded example
The append goes inside the condition.
Fill in the blanks
def long_words(t):
res = []
for w in t:
if len(w) > 5:
res.append(w)
return res
Why: append adds one element to the accumulator, and its position inside the if is what makes this a filter rather than a map. Note that append modifies res and returns None, so it must be called on its own rather than assigned — res = res.append(w) would be the t = t.sort() mistake in a new place.
Explain it
One structural difference, and one observable one.
Discussion prompt
A classmate cannot remember which is which. Give them one thing to look for in the code and one thing to check in the output.
Hint: The code test is about where the append is.
Answer:
In the code: look at whether the append is inside a condition. If every element is appended, it is a map; if only some are, it is a filter.
In the output: check the length. A map always gives a list the same length as its input, and a filter can give a shorter one.
Then add why it is worth telling them apart: chapter 19 provides a compact syntax that does both, and knowing which you want is what makes that syntax readable rather than cryptic.
Section
Section 4
Concept
There are several ways to delete elements from a list, and which one you use depends on what you know: the index, the value, or a range.
>>> t = ['a', 'b', 'c']
>>> x = t.pop(1)
>>> t, x
(['a', 'c'], 'b')
>>> t = ['a', 'b', 'c']
>>> del t[1]
>>> t
['a', 'c']
>>> t = ['a', 'b', 'c']
>>> t.remove('b')
>>> t
['a', 'c']| Form | When to use it | What it returns |
|---|---|---|
| pop(i) | you know the index and want the value back | modifies and RETURNS the element |
| del t[i] | you know the index and do not need the value | modifies, returns nothing |
| remove(x) | you know the value but not the index | modifies, returns None |
pop modifies the list and returns the element that was removed; if you do not provide an index it deletes and returns the last element. The return value from remove is None. To remove more than one element you can use del with a slice.
Think Python, 2nd edition — Allen B. Downey §10.6-10.9, pp. 94-94
Picture it
The choice is not arbitrary — each fits a different starting point.
Figure (svg): Two columns matching each deletion method to the situation it fits
The book's debugging advice applies directly: pick an idiom and stick with it. Part of the problem with lists is that there are too many ways to do things.
Worked example
The only one of the four that gives you the element back.
>>> t = ['a', 'b', 'c']
>>> x = t.pop(1)
>>> t
['a', 'c']
>>> x
'b'
>>> t.pop()
'c'| Call | What it does | Result |
|---|---|---|
| t.pop(1) | removes and returns the element at index 1 | 'b' |
| the list | one element shorter | ['a', 'c'] |
| t.pop() | no index: removes and returns the LAST | 'c' |
Notice that it is both.
Why: pop modifies the list AND returns the element that was removed, which makes it the only non-void method in this group.
Assign the result if you want it.
Why: Unlike the void methods, pop's return value is worth keeping — that is the whole reason to choose it over del.
Note the no-argument form.
Why: If you do not provide an index, it deletes and returns the last element, which is how a list is used as a stack.
Figure (svg): A three-element list with the middle element highlighted as the one being removed by pop
The list loses one element and the caller receives it. pop is for when the removed value is still wanted; del is for when it is not.
Verify: Compare with del on the same list.
Why: del t[1] removes the same element and gives you nothing — it is a statement rather than an expression, so x = del t[1] is a syntax error. That difference is what makes the choice between them meaningful rather than stylistic.
Prediction
It is the only one of the four that returns something useful.
t = ['a', 'b', 'c']
x = t.pop()
print(x, t)| Step | What happens | Result |
|---|---|---|
| t.pop() | no index given | removes the last element |
| x | the removed element | 'c' |
| t | one shorter | ['a', 'b'] |
Predict first
What does this print?
Correct: c and then ['a', 'b'] — the last element removed, and handed back to you.
Why: pop is both a modification and an expression: the list loses its last element and the call produces it. That dual nature is what distinguishes it from del and remove, both of which give you nothing back — and it is what makes a list usable as a stack.
Worked example
del with a slice removes several elements at once.
>>> t = ['a', 'b', 'c', 'd', 'e', 'f']
>>> del t[1:5]
>>> t
['a', 'f']| Part | What it selects | Result |
|---|---|---|
| t[1:5] | indices 1, 2, 3 and 4 | four elements |
| del | removes all of them | in place |
| the result | two elements left | ['a', 'f'] |
Read the slice as always.
Why: As usual, the slice selects all the elements up to but not including the second index — so indices 1 to 4.
Apply del to it.
Why: All four elements are removed and the list closes up.
Check the length.
Why: Six minus four is two, which is the subtraction from lesson 8b arriving in a new place.
Figure (svg): The state of the program after each line of Worked example deleting a range, drawn as a ladder with one rung per traced line
['a', 'f']. Four elements are removed at once, and the half-open convention means the count is simply the difference of the indices.
Verify: Compare with slice assignment to an empty list.
Why: t[1:5] = [] has exactly the same effect, which is a fifth way to delete elements. That is precisely what the book means about there being too many ways to do things — and it is why picking one idiom matters more than knowing all of them.
Trap
A loop over a list removes elements as it goes, and some elements are silently skipped.
Traverse and delete in the same loop
Why: It seems efficient, and the code reads naturally.
Removing an element shifts everything after it down by one, while the loop's position advances — so the element immediately after each removal is never examined.
Do not modify a list while iterating over it.
Build a new list with a filter instead
Why: Which produces the elements you want to keep, and leaves the original alone until you are done.
Or iterate over a copy
Why: for x in t[:] traverses a copy while modifying t, which is a use for the slice-copy idiom from lesson 10a.
The symptom is characteristic: roughly half the elements that should have been removed are still there, because every removal causes one element to be skipped.
Matching
Four situations, four tools.
Match the pairs
Why: Each fits a different starting point, which is what makes four tools defensible rather than redundant. Note that remove deletes only the FIRST matching element and raises a ValueError if the value is absent — two details worth knowing before choosing it.
Prediction
The positions shift while the loop advances.
t = [1, 2, 2, 3]
for x in t:
if x == 2:
t.remove(x)
print(t)| Pass | What happens | Consequence |
|---|---|---|
| position 0 | x is 1, kept | advance |
| position 1 | x is 2, removed | everything shifts down |
| position 2 | now holds 3, not the second 2 | the second 2 is skipped |
Predict first
What does this print?
Correct: [1, 2, 3] — one of the twos is skipped, because removing an element shifts the rest down while the loop's position advances.
Why: After the first 2 is removed the list is [1, 2, 3], and the loop moves to position 2, which now holds 3 — so the second 2 is never examined. The symptom is characteristic: roughly half the targets survive. The fix is to build a new list with a filter, or to iterate over a copy with t[:].
Real world
The book calls it part of the problem with lists.
Discussion prompt
Four ways to remove an element, plus slice assignment, is arguably too many. Suggest why a language ends up like this, and what a programmer should do about it.
Hint: Each was added for a reason that seemed good at the time.
Answer:
Each fits a genuinely different situation — index versus value, wanting the element back or not — and each was reasonable to add on its own.
Collectively they are a burden: a reader has to know all of them, and a writer has to choose, which is a decision with no interesting content.
The book's advice is the practical answer: pick an idiom and stick with it. Consistency within a program is worth more than choosing the theoretically best tool for each individual case, because it removes the question entirely.
Section
Section 5
Concept
A string is a sequence of characters and a list is a sequence of values, but a list of characters is not the same as a string. Three operations convert between them.
>>> list('spam')
['s', 'p', 'a', 'm']
>>> 'pining for the fjords'.split()
['pining', 'for', 'the', 'fjords']
>>> ' '.join(['pining', 'for', 'the', 'fjords'])
'pining for the fjords'| Operation | What it does | Direction |
|---|---|---|
| list(s) | breaks a string into characters | one element per character |
| s.split() | breaks a string into words | one element per word |
| d.join(t) | the inverse of split | one string from a list |
Because list is the name of a built-in function, you should avoid using it as a variable name — which is why the book uses t. join is a string method, so you have to invoke it on the delimiter and pass the list as a parameter.
Think Python, 2nd edition — Allen B. Downey §10.6-10.9, pp. 94-95
Picture it
Two directions, and split has an optional delimiter.
Figure (svg): A diagram showing a string converted to a list of characters or words and a list joined back into a string
join being a string method rather than a list method is the detail people trip on — it is invoked on the delimiter, not on the list.
Worked example
By default it splits on whitespace. An argument changes that.
>>> 'pining for the fjords'.split()
['pining', 'for', 'the', 'fjords']
>>> 'spam-spam-spam'.split('-')
['spam', 'spam', 'spam']| Call | What it splits on | Result |
|---|---|---|
| split() | no argument: splits on whitespace | four words |
| split('-') | an explicit delimiter | three parts |
| the delimiter | specifies the word boundaries | and is not included |
Use the default for words.
Why: With no argument, split breaks a string at whitespace, which is the common case for text.
Supply a delimiter for anything else.
Why: An optional argument called a delimiter specifies which characters to use as word boundaries.
Notice the delimiter is not in the result.
Why: The hyphens are gone from the output — they were boundaries, not content, which is the same distinction as quotation marks in lesson 1a.
Figure (svg): The state of the program after each line of Worked example split with a delimiter, drawn as a ladder with one rung per traced line
Four words with the default, three parts with an explicit hyphen. The delimiter marks the boundaries and does not appear in the result.
Verify: Split on a delimiter that is not present.
Why: 'abc'.split('-') gives ['abc'] — a one-element list containing the whole string. That splitting always produces at least one element is worth knowing, because it means the result can be traversed without a special case for no delimiter found.
Prediction
The delimiter goes between the elements, not around them.
Predict first
What does '-'.join(['a', 'b', 'c']) produce?
Correct: 'a-b-c' — the delimiter goes between each pair of elements, so three elements have two delimiters.
Why: This is the fencepost count again: n elements have n-1 gaps between them. The delimiter does not appear at the start or the end, which is what makes join the exact inverse of split — splitting 'a-b-c' on the hyphen gives back the three elements.
Worked example
The surprising part of the syntax, and it follows from join being a string method.
>>> t = ['pining', 'for', 'the', 'fjords']
>>> ' '.join(t)
'pining for the fjords'
>>> ''.join(t)
'piningforthefjords'
>>> '-'.join(t)
'pining-for-the-fjords'| Call | The delimiter | Result |
|---|---|---|
| ' '.join(t) | a space between each pair | the usual case |
| ''.join(t) | nothing between them | concatenation |
| '-'.join(t) | hyphens between them | any delimiter |
Notice which object it is called on.
Why: join is a string method, so you have to invoke it on the delimiter and pass the list as a parameter.
See why that makes sense.
Why: The delimiter is a string, and methods belong to the type they operate on. It is join's own argument that is the list.
Use the empty string to concatenate.
Why: To concatenate strings without spaces, you can use the empty string as a delimiter.
Figure (svg): Two columns contrasting the intuitive but wrong join syntax with the correct one
The delimiter goes before the dot and the list goes inside the parentheses. It reads oddly the first time and follows directly from join being a string method.
Verify: Check that join and split are inverses.
Why: ' '.join('a b c'.split()) gives back 'a b c'. join is the inverse of split, and confirming a round trip is the quickest way to check you have the delimiter right in both directions.
Trap
A student builds a long string by concatenating in a loop, one piece at a time.
Use the string accumulator from lesson 8b
Why: It works, and it was the only option before lists existed.
Every concatenation copies the whole string so far, so building a string of n pieces copies roughly n squared characters. For a few pieces it is fine and for thousands it is slow.
Accumulate into a list, then join once at the end.
append each piece to a list
Why: Which modifies the list in place rather than copying anything.
Then call join on the delimiter
Why: One pass, one new string, however many pieces there were.
This is the standard idiom for building text in Python, and it is a direct consequence of the immutability difference from lesson 10a: appending to a mutable list is cheap and rebuilding an immutable string is not.
Matching
Three operations, two directions.
Match the pairs
Why: list breaks a string into individual characters and split breaks it into pieces at a delimiter, so the two differ in granularity rather than in kind. join goes the other way, and it is invoked on the delimiter — which is the piece of syntax worth rehearsing, since ['a','b'].join(' ') is an AttributeError.
Faded example
The delimiter goes before the dot.
Fill in the blanks
words = ['hello', 'there']
sentence = ' '.join(words) # should be 'hello there'
Why: join is a string method invoked on the delimiter, with the list passed as its argument — so a single space produces a space between each pair of words. Writing words.join(' ') is the intuitive form and raises an AttributeError, because lists have no join method. The delimiter is the string, and methods belong to the type they operate on.
Explain it
It looks backwards until you say it out loud.
Discussion prompt
A classmate keeps writing t.join(' ') and getting an AttributeError. Explain why join belongs to strings rather than to lists, and give them a way to remember the order.
Hint: Which of the two things is a string?
Answer:
Say: join produces a string, and the delimiter is a string — so it is a string method. The list is what it takes as an argument.
The way to remember it: read it as put this between them. The delimiter is the thing doing the putting, so it goes first.
It is also worth noting that this design lets join work on any sequence of strings, not just lists — which it could not do if it were a list method. Attaching it to the delimiter is what makes it general.
Comparison
Fill the blanks. The shape of the output distinguishes them.
Comparison matrix
| Pattern | What it does to each element | The output |
|---|---|---|
| map | transforms it | a list of the same length |
| filter | keeps it or drops it, unchanged | a list the same length or shorter |
| reduce | combines it into a running result | a single value |
Most common list operations can be expressed as a combination of the three, which is why naming them is worth a section.
Pattern
Six steps, and the second is the one that prevents the chapter's commonest bug.
Step 3 is the t = t.sort() rule, and it is worth stating as a rule rather than a caution: if a method modifies, its return value is None and assigning it destroys your reference.
Python documentation — Data Structures Data Structures
Check
The method modifies and returns None.
t = [3, 1, 2]
t = t.sort()| Step | What happens | Result |
|---|---|---|
| t.sort() | sorts in place | the list is sorted |
| returns None | sort is a void method | nothing useful |
| t = None | the name is repointed | the list is lost |
Check your understanding
What does t hold after these two lines?
Answer: B
Why: sort modifies the list in place and returns None, so the assignment stores None in t and the sorted list becomes unreachable. The correct usage is t.sort() with no assignment. This is the single most common mistake in the chapter, and the book warns about it explicitly.
Check
The position of the append decides.
def f(t):
res = []
for x in t:
if x > 0:
res.append(x)
return res| Feature | What it means | Note |
|---|---|---|
| the append | inside the if | not every element |
| the output | may be shorter | a filter |
| the elements | unchanged | selected, not transformed |
Check your understanding
Which pattern is this?
Answer: B
Why: The append is inside the condition, so only some elements reach the output and the result may be shorter than the input. The elements themselves are unchanged, which is what distinguishes a filter from a map — a map transforms every element and always produces a list of the same length.
Check
It is a string method, invoked on the delimiter.
Check your understanding
Which of these joins a list of strings with commas?
Answer: B
Why: join is a string method, so it is invoked on the delimiter with the list passed as an argument. The delimiter goes before the dot, which reads oddly at first and follows directly from join belonging to the string type.
Real world
Map, filter and reduce are how nearly all data processing is described.
Discussion prompt
Think of a report you have produced from data — a spreadsheet, a query, a summary. Break what you did into the three patterns, and say which order they came in.
Hint: Filtering usually comes before reducing.
Answer:
A typical report filters the rows you care about, maps each to the fields you want, and reduces to a total or an average — in that order.
The order matters for cost as well as meaning: filtering first means the map and reduce handle fewer rows, which is why database queries are written the same way.
Spreadsheet formulas and SQL both name these operations explicitly — WHERE is a filter, SELECT is a map, SUM is a reduce. Recognising the three in Python is what makes those languages feel familiar rather than foreign.
Commit first
Answer, then rate your confidence. Three of these run without error.
Predict first
Which of these four lines raises an error, for a list t and an element x?
Correct: t = t + x — you cannot concatenate a list and a non-list, so it raises a TypeError.
Why: The book lists all four as wrong ways to add an element and notes that only the last causes a runtime error; the other three are legal and do the wrong thing. append with a list argument adds a nested list; assigning append's return value stores None and loses the list; and a bare concatenation computes a new list and discards it. That three of four failures are silent is exactly why the book's advice is to try each one in interactive mode and see what it does — and then to pick one correct idiom and stick with it.
Explain it
The void-method rule is the thing a student coming from strings most needs.
Discussion prompt
A classmate writes t = t.sort() and then gets a TypeError about NoneType on the next line. Explain what happened, what the fix is, and how they could recognise this class of bug in future.
Hint: The error appears one line after the mistake.
Answer:
Say: sort modifies the list in place and returns None, so the assignment stored None in t. The sorting worked; the assignment threw away the reference.
The fix is t.sort() with no assignment — or t = sorted(t), which uses the function that returns a new list.
How to recognise it: a TypeError mentioning NoneType nearly always means the return value of a void operation was assigned or used. Look one line up from the failure for a method call on the right of an equals sign — which is the same diagnostic as lesson 6a's missing return, in a new place.
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: The void-method rule is the highest-value item here, because its failure is immediate and its cause is one line earlier than the error. The three patterns are worth naming now because chapter 19 provides syntax for all of them and it is unreadable without the names. The deletion methods are four tools for four situations and the book's own advice is to pick one and stick with it. And the conversions are mostly syntax, except for join being invoked on the delimiter — which nearly everybody gets wrong once.
Connect it up
One page, from memory.
Draw it
Draw three boxes for map, filter and reduce, and under each write what shape of output it produces and one example operation. Then, beside them, make two columns listing every list method from this lesson: those that modify and return None, and those that return something worth keeping. Finally, write out the four wrong ways to append an element and mark the one that raises an error — and say what the other three do instead.
Recap
Three pages, and most of what is done with lists in practice.
| If you remember one thing | It is this |
|---|---|
| From the methods | Most modify and return None. Assigning the result destroys your list. |
| From the patterns | Map keeps the length, filter can shorten, reduce gives one value. |
| From deleting | Never modify a list while iterating over it. |
| From join | The delimiter goes before the dot. Lists have no join method. |
| From the four wrong appends | Three of them are legal. Legality is not correctness. |
The next lesson explains why the difference between modifying and creating matters so much: when two names refer to the same list, a change made through one is visible through the other — which is powerful, error-prone, and the reason this chapter exists.
Think Python, 2nd edition — Allen B. Downey §10.6-10.9, pp. 92-94 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.