Make a day06 folder for today’s files, open it in your editor, and run each script from its terminal, as on Days 1 to 5.
Day 5’s temperature program could report the highest of five days, but it could not print all five again afterwards: each value was forgotten as soon as the next one arrived. Today you learn the list, Python’s way of keeping many values together, in order, under one name, so a program can store them, change them, and come back to them.
6.1 Making a List
A teacher has five quiz scores. With what you know so far, that means five variables: score1, score2, score3, score4, score5. Every line that totals them must name all five, and a loop cannot help, because there is no way to say “go through score1 to score5”. What you want is one name for the whole collection, like one sheet of paper with the scores written down it in order.
A list is one value that holds several values in order. Each value inside it is an item (also called an element). You write a list as items separated by commas inside square brackets:
scores = [88, 92, 79]
[and]mark where the list begins and ends.- The commas separate the items. Here there are three:
88,92, and79. - The order is kept exactly as you wrote it.
[88, 92, 79]and[79, 88, 92]hold the same numbers but are different lists.
You met lists briefly on Day 3, as the result of split(), and on Day 4, where an empty list was one of the falsy values. Now you make them yourself. Try these in the REPL:
>>> scores = [88, 92, 79]
>>> scores
[88, 92, 79]
>>> len(scores)
3
>>> empty = []
>>> empty
[]
>>> len(empty)
0
>>> mixed = ["Ana", 15, 1.62, True, None]
>>> mixed
['Ana', 15, 1.62, True, None]
>>> "red green blue".split()
['red', 'green', 'blue']
>>> list("abc")
['a', 'b', 'c']len(), which counted the characters of a string on Day 3, counts the items of a list.[], with nothing between the brackets, is the empty list: a list with no items yet. Its length is 0. Programs often start with an empty list and fill it as they go (6.4).- A list can hold values of different types, even
TrueandNone. Most real lists hold one kind of thing, such as all scores or all names. split()still gives back a list, andlist()converts another sequence into a list, just asint()andstr()converted values on Day 2.list("abc")makes a list of the string’s characters.
Worked example: Printing a list
fruits = ["apple", "banana", "cherry"]
print(fruits)
print(type(fruits))
print(f"I have {len(fruits)} fruits.")['apple', 'banana', 'cherry']
<class 'list'>
I have 3 fruits.- Line 2 prints the whole list.
printshows each string with quotes inside the brackets, the way you would type it, so you can tell the string'15'from the number15. - Line 3 uses
type()from Day 2: a list’s type islist. Line 4 putslen(fruits)in an f-string (Day 3).
Lists also work with + and *, the same way strings did on Day 3: + joins two lists into a new, longer list, and * repeats a list’s items.
>>> [1, 2] + [3, 4]
[1, 2, 3, 4]
>>> ["ha"] * 3
['ha', 'ha', 'ha']
>>> [0] * 5
[0, 0, 0, 0, 0][0] * 5 is a quick way to make a list of five zeros, for example five empty score slots.
Key idea
A list holds several values, in order, under one name: scores = [88, 92, 79]. len() counts its items, and [] is
the empty list.
Watch out
A missing comma between strings. Python joins two strings that are written side by side with nothing between them, so a missing comma does not cause an error. It quietly makes one item out of two:
>>> snacks = ["chips" "salsa", "nuts"]
>>> snacks
['chipssalsa', 'nuts']
>>> len(snacks)
2You meant three snacks and got two, and nothing warned you. When a list’s length surprises you, look for a missing comma. (With numbers, [88 92] is a SyntaxError, because two numbers side by side mean nothing; Python points at the gap and asks whether you forgot a comma.)
Pause and try
Question 1 · Multiple choice
What is len(["a b", "c", ""])?
- 2
- 3
- 4
- 5
Check your reasoning
B. The commas decide how many items there are, and there are three items: the string "a b", the string "c", and the empty string "".
>>> len(["a b", "c", ""])
3- A is wrong: the empty string
""is still an item. It is a string with no characters, but it occupies a place in the list. - C is wrong: it splits
"a b"at its space.len()of a list counts items, not words; the space is just a character inside one string. - D is wrong: it counts characters (
a, space,b,c), butlen()of a list counts items, not the characters inside them.
Reread 6.1 Making a List if you chose differently.
6.2 Indexing and Slicing Lists
Storing the scores is only half the job; you also need to get one back. Day 3 taught you to read one character of a string with an index in square brackets, counting from 0, and a part of a string with a slice. Lists use exactly the same rules, because a list, like a string, is a sequence: a collection of items kept in order, each at a numbered position. The only difference is what you get back: indexing a string gives a character, and indexing a list gives an item.
| Position from the left | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Item | 88 |
92 |
79 |
95 |
60 |
| Position from the end | -5 | -4 | -3 | -2 | -1 |
>>> scores = [88, 92, 79, 95, 60]
>>> scores[0]
88
>>> scores[4]
60
>>> scores[-1]
60
>>> scores[-2]
95
>>> scores[1:3]
[92, 79]
>>> scores[:2]
[88, 92]
>>> scores[2:]
[79, 95, 60]
>>> scores[::2]
[88, 79, 60]
>>> scores[::-1]
[60, 95, 79, 92, 88]scores[0]is the first item. The last item of a 5-item list is at index 4, which islen(scores) - 1, or simply-1.- A slice
scores[start:stop]gives the items fromstartup to, but not including,stop, just like a string slice and just likerange(Day 5).scores[1:3]is index 1 and index 2: two items. - Leaving out
startmeans “from the beginning”, leaving outstopmeans “to the end”, and a step works as on Day 3:[::2]takes every second item, and[::-1]gives the items in reverse order.
A slice of a list is a new list, even when it holds only one item (scores[0:1] is [88], a list, while scores[0] is 88, a number). Indexing gives you one item; slicing always gives you a list.
Worked example: A week of temperatures
A list of seven daily temperatures, Monday first. Look at what each line asks for and whether the answer is one number or a list.
temps = [14, 16, 15, 19, 21, 23, 20]
print("Monday:", temps[0])
print("Sunday:", temps[-1])
print("Midweek:", temps[2:5])
print("Weekend:", temps[5:])
print("Days recorded:", len(temps))Monday: 14
Sunday: 20
Midweek: [15, 19, 21]
Weekend: [23, 20]
Days recorded: 7| Line | Expression | Positions | Result |
|---|---|---|---|
| 2 | temps[0] |
0 | 14 (an item) |
| 3 | temps[-1] |
6 | 20 (an item) |
| 4 | temps[2:5] |
2, 3, 4 | [15, 19, 21] |
| 5 | temps[5:] |
5, 6 | [23, 20] |
| 6 | len(temps) |
(all) | 7 |
Wednesday is index 2, and the slice 2:5 stops before Saturday’s index 5. temps[5:] takes Saturday and Sunday without needing to know the length.
When the items are strings, you can index twice: once to pick the item, then again to pick a character from that string. Python works left to right, so names[2][0] means “item 2, then its character 0”:
>>> names = ["Ana", "Ben", "Chloe"]
>>> names[2]
'Chloe'
>>> names[2][0]
'C'
>>> names[-1][-1]
'e'names[2] is 'Chloe', and 'Chloe'[0] is 'C'. names[-1][-1] is the last character of the last name. You will use exactly this double indexing for grids in 6.10.
Key idea
Lists index and slice exactly like strings: items[0] is the first item, items[-1] the last, and
items[start:stop] is a new list that stops before stop. An index gives one item; a slice gives a list.
Watch out
Using the length as an index. The last item is at len(scores) - 1, not len(scores):
scores = [88, 92, 79]
print("Last score:", scores[len(scores) - 1])
print("Next score:", scores[len(scores)])Last score: 79
Traceback (most recent call last):
File ".../day06/index_past_end.py", line 3, in <module>
print("Next score:", scores[len(scores)])
~~~~~~^^^^^^^^^^^^^
IndexError: list index out of rangeThe list has 3 items, at indexes 0, 1, and 2, so there is no index 3. This is the same IndexError you met with strings on Day 3, now saying “list” instead of “string”. Use scores[-1] for the last item. A slice past the end, such as scores[1:100], never causes an error: like a string slice, it just stops at the end. (Older Python versions may word error messages a little differently.)
Pause and try
Question 2 · Tracing
Predict exactly what the REPL shows for each line after the first.
>>> colors = ["red", "orange", "yellow", "green", "blue"]
>>> colors[-2]
>>> colors[1:4]
>>> colors[::2]
>>> colors[3][0]Check your reasoning
>>> colors = ["red", "orange", "yellow", "green", "blue"]
>>> colors[-2]
'green'
>>> colors[1:4]
['orange', 'yellow', 'green']
>>> colors[::2]
['red', 'yellow', 'blue']
>>> colors[3][0]
'g'-2is the second item from the end:'green'. The REPL shows a single string item with quotes.1:4is indexes 1, 2, and 3: three items, as a list. Index 4,'blue', is the stop and is left out.::2takes indexes 0, 2, and 4.colors[3]is'green', and its character 0 is'g'.
Reread 6.2 Indexing and Slicing Lists if any answer differed.
6.3 Lists Are Mutable
A playlist has a typo in one song title. With a string, Day 3 taught you that you cannot fix one character in place: you have to build a whole new string. Is a list the same?
No, and this is the biggest difference between a list and a string. Put the two side by side:
>>> word = "cat"
>>> word[0] = "b"
Traceback (most recent call last):
...
TypeError: 'str' object does not support item assignment
>>> letters = ["c", "a", "t"]
>>> letters[0] = "b"
>>> letters
['b', 'a', 't']Assigning to one position of a string is a TypeError. Assigning to one position of a list replaces that item. A list is mutable: its contents can be changed after it is made. A string is immutable: it can never change (Day 3). Writing items[index] = value is called item assignment.
Think of a list as a row of numbered boxes. letters[0] = "b" takes out what was in box 0 and puts "b" there instead. It is still the same list object (Day 4); only one box’s contents changed.
Why make lists changeable? Because lists are usually collections that grow and change as a program runs, such as a shopping list or the scores so far, and rebuilding the whole collection for every change would be slow and awkward. Day 3 said immutable strings are safe to share; 6.9 shows the price lists pay for being changeable.
Worked example: Fixing a playlist
playlist = ["Intro", "Sunrise", "Rainfal", "Night Drive"]
print("Before:", playlist)
playlist[2] = "Rainfall"
playlist[-1] = "Night Drive (Live)"
print("After:", playlist)Before: ['Intro', 'Sunrise', 'Rainfal', 'Night Drive']
After: ['Intro', 'Sunrise', 'Rainfall', 'Night Drive (Live)']- Line 3 replaces the item at index 2, the misspelled
'Rainfal', with the corrected title. Items 0, 1, and 3 are untouched. - Line 4 uses a negative index on the left side:
playlist[-1]is the last box, so the last title is replaced. - Item assignment replaces an item; it never adds or removes one, so the list still has four items.
The right side of an item assignment can use the item’s old value, exactly like count = count - 1 on Day 2: work out the right side first, then store the result in the box. Augmented assignment from Day 5 works on items too:
scores = [70, 85, 90]
scores[0] += 5
scores[2] = scores[2] - 10
print(scores)[75, 85, 80]
Line 2 means scores[0] = scores[0] + 5: read item 0 (70), add 5, store 75 back in box 0. Line 3 reads item 2 (90), subtracts 10, and stores 80. Item 1 is never mentioned, so it stays 85.
Key idea
Lists are mutable: items[index] = value replaces the item at that position, in the same list. Strings are immutable,
so the same assignment on a string is a TypeError.
Watch out
Assigning past the end to add an item. It seems natural that assigning to the next free position would add an item there:
seats = ["Ana", "Ben", "Chloe"]
seats[3] = "Dev"
print(seats)Traceback (most recent call last):
File ".../day06/assign_past_end.py", line 2, in <module>
seats[3] = "Dev"
~~~~~^^^
IndexError: list assignment index out of rangeItem assignment only replaces an item that already exists, and the three-item list has no box 3. To add an item, use append, which is next, in 6.4.
Pause and try
Question 3 · Tracing
What does this program print? Track the list after each line.
shelf = ["jam", "tea", "rice", "oats"]
shelf[1] = "coffee"
shelf[-1] = shelf[0]
shelf[0] = "honey"
print(shelf)
print(len(shelf))Check your reasoning
['honey', 'coffee', 'rice', 'jam']
4| Line | Change | shelf after |
|---|---|---|
| 1 | Make the list | ['jam', 'tea', 'rice', 'oats'] |
| 2 | Box 1 becomes 'coffee' |
['jam', 'coffee', 'rice', 'oats'] |
| 3 | Last box gets a copy of shelf[0] |
['jam', 'coffee', 'rice', 'jam'] |
| 4 | Box 0 becomes 'honey' |
['honey', 'coffee', 'rice', 'jam'] |
Line 3 reads shelf[0] at that moment, when it is still 'jam'. Changing box 0 on line 4 does not go back and change box 3. The length stays 4, because item assignment replaces items and never adds any.
Reread 6.3 Lists Are Mutable if your output differed.
6.4 Adding Items: append, insert, extend
Most lists are not typed out in full at the start: a program collects names as people sign up, or scores as a teacher types them. It needs to grow the list.
Day 3 introduced the method: a function that belongs to a value, called with a dot, like name.upper(). String methods always gave back a new string, because strings cannot change. List methods are different: most of them change the list itself. Three of them add items:
| Method | What it does |
|---|---|
items.append(value) |
Adds one value to the end |
items.insert(i, value) |
Puts the value at index i, shifting later items right |
items.extend(other) |
Adds each item of another sequence to the end |
>>> guests = ["Ana", "Ben"]
>>> guests.append("Chloe")
>>> guests
['Ana', 'Ben', 'Chloe']
>>> guests.insert(0, "Dev")
>>> guests
['Dev', 'Ana', 'Ben', 'Chloe']
>>> guests.insert(2, "Eli")
>>> guests
['Dev', 'Ana', 'Eli', 'Ben', 'Chloe']
>>> guests.extend(["Fay", "Gus"])
>>> guests
['Dev', 'Ana', 'Eli', 'Ben', 'Chloe', 'Fay', 'Gus']The REPL shows nothing after each call, because these methods change the list and give back nothing to show. insert(0, "Dev") moves everyone one place right, so Ana is now at index 1. If the index you give insert is past the end, the item simply goes at the end; insert never causes an IndexError.
Worked example: Collecting names until a blank line
The most common way to fill a list is to start with an empty list and append inside a loop. This is the Day 5 accumulator pattern, with a list as the accumulator: set it before the loop, add to it inside, use it after. This run types Ana, Ben, Chloe, then presses Enter on an empty line:
names = []
while True:
name = input("Name (blank to finish): ").strip()
if name == "":
break
names.append(name)
print(names)
print(f"{len(names)} names collected.")Name (blank to finish): Ana
Name (blank to finish): Ben
Name (blank to finish): Chloe
Name (blank to finish):
['Ana', 'Ben', 'Chloe']
3 names collected.names = [] comes before the loop; inside it, each pass would throw away the names so far, like the reset bug from Day 5. Unlike the Day 5 programs, this one still has every name after the loop, in the order they were typed.
append and extend look similar but treat their argument differently. append always adds one item, even if that item is itself a list. extend goes through its argument and adds each item:
>>> a = [1, 2, 3]
>>> a.append([4, 5])
>>> a
[1, 2, 3, [4, 5]]
>>> len(a)
4
>>> b = [1, 2, 3]
>>> b.extend([4, 5])
>>> b
[1, 2, 3, 4, 5]
>>> len(b)
5
>>> c = [1, 2, 3]
>>> c.extend("hi")
>>> c
[1, 2, 3, 'h', 'i']
>>> d = [1, 2]
>>> d + [3]
[1, 2, 3]
>>> d
[1, 2]a.append([4, 5])adds the whole list[4, 5]as a single item, soahas 4 items and the last one is a list. A list inside a list is called a nested list; 6.10 puts that to good use.b.extend([4, 5])adds 4 and 5 separately, sobhas 5 items.extendaccepts any sequence. A string is a sequence of characters, soc.extend("hi")adds'h'and'i'. To add the word as one item, usec.append("hi").d + [3]builds a new list and leavesdunchanged, just as+on strings did.appendandextendchange the list itself. That difference matters a great deal in 6.9.
Key idea
append(value) adds one item to the end, insert(i, value) adds one at index i, and extend(sequence) adds each
item of a sequence. All three change the list itself and give back nothing.
Watch out
Saving the result of append. String methods give back a new string, so name = name.upper() is the right habit. Carry that habit over to lists and the list disappears:
names = ["Ana", "Ben"]
names = names.append("Chloe")
print(names)Noneappend changes the list and gives back None (Day 2’s “no value”). Line 2 then stores that None in names, and the list is lost. A list method that changes the list is called on its own line, with nothing assigned: names.append("Chloe").
Pause and try
Question 4 · Multiple choice
What does this program print?
nums = [1, 2]
nums.append([3, 4])
nums.extend([5])
print(len(nums))345[1, 2, [3, 4], 5]
Check your reasoning
B. append([3, 4]) adds the list [3, 4] as one item, so nums is [1, 2, [3, 4]], 3 items. extend([5]) adds each item of [5], which is one item, 5. Now nums is [1, 2, [3, 4], 5]: 4 items.
- A is wrong: it forgets that
extendadds 5. - C is wrong: it treats
append([3, 4])as adding 3 and 4 separately, which is whatextenddoes. - D is wrong: that is what
numsholds, but the program printslen(nums), which is a number: how many items the list has.
Reread 6.4 Adding Items: append, insert, extend if you chose differently.
Question 5 · Tracing
What does this program print? Track the list after each line.
line = ["Kai", "Lea"]
line.insert(1, "Max")
line.append("Nia")
line.insert(0, "Oto")
line.insert(10, "Pia")
print(line)Check your reasoning
['Oto', 'Kai', 'Max', 'Lea', 'Nia', 'Pia']| Line | Call | line after |
|---|---|---|
| 2 | insert(1, "Max") |
['Kai', 'Max', 'Lea'] |
| 3 | append("Nia") |
['Kai', 'Max', 'Lea', 'Nia'] |
| 4 | insert(0, "Oto") |
['Oto', 'Kai', 'Max', 'Lea', 'Nia'] |
| 5 | insert(10, "Pia") |
['Oto', 'Kai', 'Max', 'Lea', 'Nia', 'Pia'] |
Index 10 is past the end of a 5-item list, so insert puts Pia at the end instead of causing an error.
Reread 6.4 Adding Items: append, insert, extend if your output differed.
6.5 Removing Items: pop, remove, clear
A queue at a deli counter grows at the back and shrinks at the front. A program that keeps the queue as a list needs to take items out, sometimes by position (“serve whoever is first”) and sometimes by value (“Ben left the queue”).
| Method | Removes | Gives back |
|---|---|---|
items.pop() |
The last item | The removed item |
items.pop(i) |
The item at index i |
The removed item |
items.remove(value) |
The first item equal to value |
None |
items.clear() |
Every item, leaving [] |
None |
>>> tasks = ["wash", "dry", "fold", "iron"]
>>> tasks.pop()
'iron'
>>> tasks
['wash', 'dry', 'fold']
>>> tasks.pop(0)
'wash'
>>> tasks
['dry', 'fold']
>>> tasks.remove("fold")
>>> tasks
['dry']
>>> tasks.clear()
>>> tasks
[]
>>> tasks.pop()
Traceback (most recent call last):
...
IndexError: pop from empty listpop()takes the last item out and gives it back, so the REPL shows'iron', and you can store it:last = tasks.pop().- After
pop(0), everything shifts one place left, so'dry'is now at index 0. remove("fold")finds the value for you; you do not need to know where it is.pop()on an empty list is anIndexError: there is no last item to take.
Worked example: Serving a queue
queue = ["Ana", "Ben", "Chloe"]
queue.append("Dev")
served = queue.pop(0)
print("Now serving:", served)
served = queue.pop(0)
print("Now serving:", served)
print("Still waiting:", queue)Now serving: Ana
Now serving: Ben
Still waiting: ['Chloe', 'Dev']New customers join at the end with append, and pop(0) serves the person at the front, giving back the name so it can be printed. With pop() instead, the last person to arrive would be served first, like taking the top plate off a stack.
remove takes out only the first match, searching from the left:
>>> votes = ["yes", "no", "yes", "yes"]
>>> votes.remove("yes")
>>> votes
['no', 'yes', 'yes']Only the first "yes" went. To remove every copy, keep removing while there is still one in the list. in asks whether a value is an item of a list, just as it asked whether one string was inside another on Day 3 (6.7 says more):
votes = ["yes", "no", "yes", "maybe", "yes"]
while "yes" in votes:
votes.remove("yes")
print(votes)['no', 'maybe']
Each pass removes one "yes". After the third, "yes" in votes is False and the loop ends. The remove call is the line that moves the condition toward False, which is what Day 5 said every while loop needs.
Key idea
pop() removes and gives back the last item, and pop(i) the item at index i. remove(value) removes the first
item equal to value. clear() removes everything.
Watch out
Removing a value that is not there. remove must find the value, or it stops the program:
cart = ["bread", "milk"]
cart.remove("eggs")
print(cart)Traceback (most recent call last):
File ".../day06/remove_missing.py", line 2, in <module>
cart.remove("eggs")
~~~~~~~~~~~^^^^^^^^
ValueError: list.remove(x): x not in listA ValueError (Day 2) means the type was fine but this particular value was not. The message says the value you asked for, called x in the message, is not in the list. When you cannot be sure the value is there, check with in first:
cart = ["bread", "milk"]
item = "eggs"
if item in cart:
cart.remove(item)
else:
print(f"{item} is not in the cart.")
print(cart)eggs is not in the cart.
['bread', 'milk']Pause and try
Question 6 · Tracing
What does this program print? Track plates, top, and second.
plates = ["red", "blue", "green", "white"]
top = plates.pop()
plates.remove("red")
plates.append(top)
second = plates.pop(0)
print(top, second)
print(plates)Check your reasoning
white blue
['green', 'white']| Line | Call | Gives back | plates after |
|---|---|---|---|
| 2 | pop() |
'white' |
['red', 'blue', 'green'] |
| 3 | remove("red") |
None |
['blue', 'green'] |
| 4 | append(top) |
None |
['blue', 'green', 'white'] |
| 5 | pop(0) |
'blue' |
['green', 'white'] |
pop() with no index takes the last plate. After remove("red"), blue is at index 0, so pop(0) gives 'blue'.
Reread 6.5 Removing Items: pop, remove, clear if your output differed.
Question 7 · Debugging
This program should remove the song Sunrise from the playlist. It stops with an error. Quote the last line of the error, explain it, and fix the program so it can never crash on a missing song.
playlist = ["Intro", "Sunrise", "Rainfall"]
playlist.remove("sunrise")
print(playlist)Check your reasoning
ValueError: list.remove(x): x not in listremove looks for an item equal to "sunrise", and string comparison is case-sensitive (Day 4): "sunrise" == "Sunrise" is False. No item matches, so remove stops the program. Use the exact title, and check with in before removing so a missing song prints a message instead of crashing:
playlist = ["Intro", "Sunrise", "Rainfall"]
song = "Sunrise"
if song in playlist:
playlist.remove(song)
else:
print(f"{song} is not in the playlist.")
print(playlist)['Intro', 'Rainfall']Reread 6.5 Removing Items: pop, remove, clear if you missed it.
6.6 Looping Over a List
Day 5’s for loop went through a string one character at a time. Its real strength is lists: “do this for each guest”, “check each score”. A for loop over a list runs its body once for each item, with the loop variable holding the current item.
guests = ["Ana", "Ben", "Chloe"]
for name in guests:
print(f"Welcome, {name}!")
print("Everyone is here.")Welcome, Ana!
Welcome, Ben!
Welcome, Chloe!
Everyone is here.
The loop variable name holds 'Ana', then 'Ben', then 'Chloe'. Adding a guest to the list adds a welcome line without touching the loop.
Worked example: Building a list of passing scores
A loop can also build a new list while it goes through an old one. This picks out the scores of 60 or more:
scores = [72, 45, 88, 59, 91]
passing = []
for score in scores:
if score >= 60:
passing.append(score)
print("Passing scores:", passing)
print(f"{len(passing)} of {len(scores)} passed.")Passing scores: [72, 88, 91]
3 of 5 passed.score |
score >= 60 |
passing after |
|---|---|---|
| 72 | True |
[72] |
| 45 | False |
[72] |
| 88 | True |
[72, 88] |
| 59 | False |
[72, 88] |
| 91 | True |
[72, 88, 91] |
This is the accumulator pattern again, with the if deciding which passes add an item. The original scores list is never changed, so the program can report “3 of 5”.
To number the items as you print them, use enumerate() from Day 5. It works on a list exactly as it did on a string:
chores = ["dishes", "laundry", "trash"]
for number, chore in enumerate(chores, 1):
print(f"{number}. {chore}")1. dishes
2. laundry
3. trash
Changing every item. Day 5 warned that assigning to a loop variable does not change the string you are looping over. The same is true for a list:
prices = [4, 10, 7]
for price in prices:
price = price * 2
print(prices)[4, 10, 7]
price is just a name for the current item. price = price * 2 makes price refer to a new number; the box in the list still holds the old one. To change the boxes, you need their indexes, so loop over range(len(prices)) (Day 5) and use item assignment (6.3):
prices = [4, 10, 7]
for i in range(len(prices)):
prices[i] = prices[i] * 2
print(prices)[8, 20, 14]
range(len(prices)) gives 0, 1, 2, the indexes of the three items, and prices[i] = prices[i] * 2 replaces each box with double its value.
Key idea
for item in items: runs once per item. To change items in place, loop over the indexes with range(len(items)) and
assign to items[i]. To keep only some items, build a new list.
Watch out
Removing items from the list you are looping over. This should remove every failing score:
scores = [45, 50, 88, 30]
for score in scores:
if score < 60:
scores.remove(score)
print(scores)[50, 88]The 50 is still there. A for loop over a list keeps track of a position, moving one place right after each pass, and remove shifts every later item one place left:
| Pass | Position | List at the start of the pass | score |
Action |
|---|---|---|---|---|
| 1 | 0 | [45, 50, 88, 30] |
45 | Remove 45; 50 slides to position 0 |
| 2 | 1 | [50, 88, 30] |
88 | Keep |
| 3 | 2 | [50, 88, 30] |
30 | Remove |
When 45 was removed, 50 slid into position 0, which the loop had already visited, so 50 was never checked. Never add or remove items in a list while a for loop is going through it. Build a new list of the items to keep instead, as passing.py does.
Pause and try
Question 8 · Tracing
What does this program print?
words = ["sun", "moon", "star", "sky"]
long_words = []
for index, word in enumerate(words):
if len(word) > 3:
long_words.append(word.upper())
else:
words[index] = "-"
print(words)
print(long_words)Check your reasoning
['-', 'moon', 'star', '-']
['MOON', 'STAR']index |
word |
len(word) > 3 |
Action |
|---|---|---|---|
| 0 | 'sun' |
False |
words[0] becomes '-' |
| 1 | 'moon' |
True |
Append 'MOON' to long_words |
| 2 | 'star' |
True |
Append 'STAR' to long_words |
| 3 | 'sky' |
False |
words[3] becomes '-' |
Replacing an item by its index is safe during a loop, because the list keeps the same length and nothing shifts. upper() gives a new string, which is what gets appended; the items in words stay lowercase.
Reread 6.6 Looping Over a List if your output differed.
6.7 len, in, min, max, and sum
On Day 5 you wrote loops to count, to total, and to find the largest value. Once the values are in a list, Python has a built-in function (a function that is always available, like print and len) for each one.
>>> scores = [72, 45, 88, 59, 91]
>>> len(scores)
5
>>> min(scores)
45
>>> max(scores)
91
>>> sum(scores)
355
>>> sum(scores) / len(scores)
71.0
>>> 88 in scores
True
>>> 100 in scores
False
>>> 100 not in scores
True
>>> max(["pear", "Apple", "fig"])
'pear'
>>> min(["pear", "Apple", "fig"])
'Apple'| Call | Gives |
|---|---|
len(items) |
The number of items |
min(items) |
The smallest item |
max(items) |
The largest item |
sum(items) |
All the numbers added up |
value in items |
True if some item equals value |
value not in items |
True if no item equals value |
- There is no built-in average, but
sum(scores) / len(scores)is one. not inis the opposite ofin:100 not in scoresreads like English and means the same asnot (100 in scores).minandmaxcompare items with the same rules as<and>from Day 4. For strings that means alphabetical order, with every capital before every small letter, somaxof"pear","Apple", and"fig"is'pear', andminis'Apple'.
in on a list is not quite the in you used on strings. On a string, in looks for a piece of text anywhere inside. On a list, it asks whether one of the items is equal to the value, so a part of an item does not count:
>>> "app" in "apple"
True
>>> "app" in ["apple", "pear"]
False
>>> "apple" in ["apple", "pear"]
True"app" is inside the string "apple", but no item of the list is equal to "app".
Worked example: Score statistics in four lines
Compare this with the Day 5 loops that counted, totaled, and tracked the highest value:
scores = [72, 45, 88, 59, 91]
average = sum(scores) / len(scores)
print("Highest:", max(scores))
print("Lowest:", min(scores))
print(f"Average: {average:.1f}")
print("Anyone perfect?", 100 in scores)Highest: 91
Lowest: 45
Average: 71.0
Anyone perfect? False355 / 5 is 71.0, and line 6 prints the bool result of in directly. Every one of these lines would have been a loop with an accumulator on Day 5; the built-ins do the looping for you.
The empty list. A program that collects scores might end up with none. The built-ins do not all agree about what to do then:
>>> scores = []
>>> sum(scores)
0
>>> len(scores)
0
>>> max(scores)
Traceback (most recent call last):
...
ValueError: max() iterable argument is empty
>>> sum(scores) / len(scores)
Traceback (most recent call last):
...
ZeroDivisionError: division by zerosum and len of an empty list are 0. But there is no largest item of nothing, so max (and min) stop with a ValueError; “iterable” in the message means “something you can loop over”, here the list. And the average divides by 0, the ZeroDivisionError from Day 2. Guard with the Day 4 rule that an empty list is falsy:
scores = []
if scores:
print(f"Average: {sum(scores) / len(scores):.1f}")
else:
print("No scores yet.")No scores yet.
if scores: is True only when the list has at least one item, so the division runs only when it is safe.
Key idea
len, min, max, and sum summarize a list, and in / not in ask whether a value is one of its items. On an
empty list, min and max fail, so check if items: first.
Watch out
Naming a variable sum. sum is a natural name for a total, and Python lets you use it. But that name already refers to the built-in function, and your assignment replaces it:
sum = 0
for n in [3, 4, 5]:
sum += n
print(sum)
scores = [1, 2]
print(sum(scores))12
Traceback (most recent call last):
File ".../day06/sum_shadow.py", line 6, in <module>
print(sum(scores))
~~~^^^^^^^^
TypeError: 'int' object is not callableAfter line 1, sum refers to an int, not to the function. “Not callable” means “you cannot call it with parentheses”: 12(scores) makes no sense. Use names such as total, highest, and lowest for your own variables, and never sum, min, max, len, or list.
Pause and try
Question 9 · Multiple choice
What does this program print?
temps = [3, -2, 8, 0]
print(max(temps) - min(temps), sum(temps) / len(temps))10 2.256 2.2510 28 2.25
Check your reasoning
A. max(temps) is 8 and min(temps) is -2, so the range is 8 - (-2) = 10. sum(temps) is 3 + (-2) + 8 + 0 = 9, and 9 / 4 is 2.25.
- B is wrong: it subtracts 2 instead of -2. Subtracting a negative number adds it.
- C is wrong:
/always gives a float (Day 2), so the average is 2.25, not 2. - D is wrong: it takes the smallest value as 0, but -2 is smaller than 0.
Reread 6.7 len, in, min, max, and sum if you chose differently.
6.8 Sorting and Reversing
A leaderboard shows scores from highest to lowest; a class register lists names alphabetically. Putting items in order is called sorting, and Python gives you two ways to do it that look almost alike but behave very differently.
items.sort()is a list method. It sorts the list in place: it rearranges the items inside that same list and gives backNone, likeappend.sorted(items)is a built-in function. It builds a new sorted list and gives it back, leaving the original list exactly as it was.
>>> scores = [72, 45, 88, 59]
>>> ranked = sorted(scores)
>>> ranked
[45, 59, 72, 88]
>>> scores
[72, 45, 88, 59]
>>> scores.sort()
>>> scores
[45, 59, 72, 88]
>>> sorted(scores, reverse=True)
[88, 72, 59, 45]
>>> scores.sort(reverse=True)
>>> scores
[88, 72, 59, 45]After sorted(scores), scores is still in the order the scores were entered; after scores.sort(), it is not. Both sort from smallest to largest. To sort largest first, add reverse=True inside the parentheses. This is a keyword argument: an argument passed with a name and =, so Python knows which setting you mean. reverse=True means “sort the other way round”.
items.sort() |
sorted(items) |
|
|---|---|---|
| Kind | List method | Built-in function |
| Original list | Rearranged in place | Unchanged |
| Gives back | None |
A new sorted list |
| Use when | You no longer need the old order | You need both orders, or want to use the result directly |
Reversing is not sorting. The method items.reverse() flips the list’s order in place, last item first, without comparing anything. A slice with step -1 (6.2) gives a reversed copy instead:
>>> stops = ["Park", "Main", "Bridge", "Zoo"]
>>> stops.reverse()
>>> stops
['Zoo', 'Bridge', 'Main', 'Park']
>>> stops[::-1]
['Park', 'Main', 'Bridge', 'Zoo']reverse() changed stops itself. The slice stops[::-1] gives a new list, reversed back, and leaves stops as it was.
Worked example: A top-three leaderboard
scores = [72, 45, 88, 59, 91, 66]
top_three = sorted(scores, reverse=True)[:3]
print("In order entered:", scores)
print("Top three:", top_three)In order entered: [72, 45, 88, 59, 91, 66]
Top three: [91, 88, 72]Line 2 works from the inside out: sorted(scores, reverse=True) builds the new list [91, 88, 72, 66, 59, 45], and [:3] slices its first three items. scores is untouched, so line 3 still shows the order the scores were entered in.
Sorting uses the same comparisons as min and max, which brings two surprises:
>>> names = ["bea", "Ana", "cal", "Ben"]
>>> sorted(names)
['Ana', 'Ben', 'bea', 'cal']
>>> sorted(["9", "10", "100"])
['10', '100', '9']
>>> sorted([9, 10, 100])
[9, 10, 100]
>>> sorted([3, "10", 7])
Traceback (most recent call last):
...
TypeError: '<' not supported between instances of 'str' and 'int'- Capital letters come before all small letters (Day 4), so
'Ben'sorts before'bea'. - Numbers stored as strings sort as text, character by character:
"10"comes before"9"because"1"comes before"9". Numbers typed withinput()are strings until you convert them withint()orfloat()(Day 3), so convert before you sort. - A list that mixes numbers and strings cannot be sorted at all:
<does not work between astrand anint, so Python stops with aTypeError.
Key idea
items.sort() rearranges the list itself and gives back None; sorted(items) gives back a new sorted list and
leaves the original alone. reverse=True sorts largest first, and items.reverse() flips the order without sorting.
Watch out
Saving the result of sort(). It reads naturally, but like append, sort() gives back None:
scores = [72, 45, 88]
ranked = scores.sort()
print(ranked)
print(scores)None
[45, 72, 88]scores was sorted, but ranked is None. If you want a sorted result to store, use ranked = sorted(scores). If you want the list itself sorted, write scores.sort() on its own line.
Pause and try
Question 10 · Multiple choice
What does this program print?
prices = [5, 2, 9]
cheap_first = sorted(prices)
prices.reverse()
print(prices, cheap_first)[9, 5, 2] [2, 5, 9][9, 2, 5] [2, 5, 9][9, 2, 5] [9, 5, 2][2, 5, 9] [2, 5, 9]
Check your reasoning
B. sorted(prices) builds a new list [2, 5, 9] and leaves prices as [5, 2, 9]. Then prices.reverse() flips prices in place, without sorting, giving [9, 2, 5]. cheap_first is a separate list, so the reverse does not touch it.
- A is wrong:
reverse()does not sort largest first; it only flips the current order. - C is wrong:
cheap_firstwas made bysortedbefore the reverse, and it is a different list fromprices. - D is wrong:
sorteddoes not changeprices.
Reread 6.8 Sorting and Reversing if you chose differently.
6.9 Aliasing Versus Copying
Here is a puzzle. A program makes a weekend routine by starting from the weekday routine and changing it: no alarm, and pancakes at the end. It only changes weekend. Read it and predict what it prints for weekday:
weekday = ["alarm", "breakfast", "bus"]
weekend = weekday
weekend.remove("alarm")
weekend.append("pancakes")
print("Weekend:", weekend)
print("Weekday:", weekday)
print(weekend is weekday)Weekend: ['breakfast', 'bus', 'pancakes']
Weekday: ['breakfast', 'bus', 'pancakes']
True
The weekday routine lost its alarm and gained pancakes, even though no line mentions weekday after line 1. This is the day’s most important idea, and it explains why changing one list can change “another” one.
Assignment never copies. Day 4 said that every value is an object in memory and that a variable name refers to an object. weekend = weekday does not make a second list. It makes the name weekend refer to the same list object that weekday refers to. There is one list with two names:
| Name | Refers to |
|---|---|
weekday |
List object #1: ['alarm', 'breakfast', 'bus'] |
weekend |
List object #1 (the same one) |
Two names for the same object are aliases, and this situation is aliasing, like a person who answers to both “Robert” and “Bob”. weekend.remove("alarm") changes object #1, so weekday sees the change. Line 7 confirms it with Day 4’s is.
Why did this never bite you with numbers or strings? Because they are immutable (Day 3): nothing can change them in place, so it never matters how many names share one. Lists are mutable, so a change made through one name shows up through every alias.
Copying. To get a second, separate list, ask for a copy. The method items.copy() builds a new list holding the same items:
Worked example: A weekend routine that leaves the weekday alone
weekday = ["alarm", "breakfast", "bus"]
weekend = weekday.copy()
weekend.remove("alarm")
weekend.append("pancakes")
print("Weekend:", weekend)
print("Weekday:", weekday)
print(weekend is weekday)Weekend: ['breakfast', 'bus', 'pancakes']
Weekday: ['alarm', 'breakfast', 'bus']
FalseOnly line 2 changed. Now there are two list objects, so changes through weekend reach only the copy, and weekday keeps its alarm.
There are three common ways to copy a list, and they all do the same job:
>>> a = [1, 2, 3]
>>> b = a
>>> c = a.copy()
>>> b is a
True
>>> c is a
False
>>> c == a
True
>>> d = a[:]
>>> d is a
False
>>> e = list(a)
>>> e is a
Falseb = amakes an alias:b is aisTrue.a.copy(), the full slicea[:](every slice is a new list, 6.2), andlist(a)each build a new list:isgivesFalse, while==givesTruebecause the contents are equal. This is Day 4’s equality versus identity, now with a reason to care.
Rebinding is not changing. Aliasing only shows when the object itself is changed in place. Assigning a new object to one of the names, called rebinding (Day 2), moves that one name and leaves the other alone:
>>> x = 5
>>> y = x
>>> x = x + 1
>>> y
5
>>> a = [5]
>>> b = a
>>> a = a + [6]
>>> a
[5, 6]
>>> b
[5]a = a + [6] builds a new list (+ always does, 6.4) and points a at it. b still refers to the original [5]. Compare a.append(6), which would change the shared list, so b would show [5, 6] too.
| Changes the list in place (aliases see it) | Makes a new list (aliases do not see it) |
|---|---|
items[i] = value |
items + other |
append, insert, extend |
items[start:stop] |
pop, remove, clear |
sorted(items) |
sort, reverse |
items.copy(), list(items) |
One more detail: a copy is a new list, but the items in it are the same objects as in the original. For numbers and strings this never matters, because nothing can change them. When the items are themselves lists, it does; 6.10 has the details.
Key idea
b = a makes b another name for the same list, so changes through either name show in both. b = a.copy() (or
a[:], or list(a)) makes a separate list. Use is to check: aliases are the same object.
Watch out
Making a “backup” with =. backup = scores looks like it saves the old scores before you change the list. It does not: it gives the same list a second name, so every change to scores also appears in backup, and the “backup” is useless. Use backup = scores.copy() whenever you want to keep the old contents.
Pause and try
Question 11 · Tracing
What does this program print? Before you trace, decide how many list objects there are.
original = [3, 1, 2]
backup = original
sorted_copy = sorted(original)
original.sort()
backup.append(4)
print(original)
print(backup)
print(sorted_copy)Check your reasoning
[1, 2, 3, 4]
[1, 2, 3, 4]
[1, 2, 3]There are two list objects. original and backup are aliases of the first; sorted builds the second, [1, 2, 3], which only sorted_copy refers to.
| Line | Action | Shared list (original, backup) |
sorted_copy |
|---|---|---|---|
| 1–2 | Make, alias | [3, 1, 2] |
|
| 3 | sorted(original) |
[3, 1, 2] |
[1, 2, 3] |
| 4 | original.sort() |
[1, 2, 3] |
[1, 2, 3] |
| 5 | backup.append(4) |
[1, 2, 3, 4] |
[1, 2, 3] |
sort and append change the shared list, so both names show [1, 2, 3, 4]. sorted_copy is a separate list, so the append does not reach it.
Reread 6.9 Aliasing Versus Copying if your output differed.
Question 12 · Multiple choice
a refers to a list. After which line can b be changed without changing a?
b = ab = a[:]b = a.sort()b = a.append(0)
Check your reasoning
B. A full slice builds a new list with the same items, so b is a is False, and changes through b affect only the new list.
- A is wrong:
b = ais an alias; both names refer to one list. - C is wrong:
sort()sortsain place and gives backNone, sobisNone, not a list at all. - D is wrong:
appendchangesaand gives backNone, sobisNone.
Reread 6.9 Aliasing Versus Copying if you chose differently.
6.10 Nested Lists as a Grid
A tic-tac-toe board, a seating chart, and a spreadsheet of test scores all have rows and columns. One list is a single row of boxes. A grid needs rows of rows.
6.4 showed that a list can hold another list as an item. A list whose items are all lists of the same length is a grid (also called a 2D list, for two dimensions): the outer list holds the rows, and each row is a list of values. To reach one value, you index twice, as you did with names[2][0] in 6.2: first the row, then the column.
>>> grid = [[1, 2, 3], [4, 5, 6]]
>>> grid[0]
[1, 2, 3]
>>> grid[1][2]
6
>>> len(grid)
2
>>> len(grid[0])
3
>>> grid[1][0] = 40
>>> grid
[[1, 2, 3], [40, 5, 6]]| column 0 | column 1 | column 2 | |
|---|---|---|---|
| row 0 | 1 | 2 | 3 |
| row 1 | 4 (then 40) | 5 | 6 |
grid[0]is the whole first row,[1, 2, 3].grid[1][2]is evaluated left to right:grid[1]is the row[4, 5, 6], and its item 2 is6. Read it as “row 1, column 2”.len(grid)is the number of rows, andlen(grid[0]), the length of one row, is the number of columns.grid[1][0] = 40picks row 1, then replaces its item 0. Rows are lists, so they are mutable.
Writing a grid with one row per line, as below, makes the rows and columns easy to see. Python allows a list to continue over several lines until its closing bracket, and a comma after the last item is allowed.
Worked example: A tic-tac-toe board
"." marks an empty square.
board = [
["X", "O", "."],
[".", "X", "."],
["O", ".", "."],
]
board[2][2] = "X"
for row in board:
print(" ".join(row))
print("Center:", board[1][1])X O .
. X .
O . X
Center: X- Line 6 puts an
Xin row 2 (the bottom row), column 2 (the right square). - The loop on line 7 goes through the outer list, so
rowis one whole row on each pass: a list of three strings." ".join(row)(Day 3) glues the three strings into one line with spaces between them. board[1][1]is the middle row’s middle square.
When you need the row and column numbers, for example to label rows or to do arithmetic with them, use nested loops (Day 5) over range(len(...)): the outer loop picks each row index, and the inner loop goes through every column index of that row.
scores = [
[90, 85, 77],
[60, 72, 81],
]
for row in range(len(scores)):
total = 0
for col in range(len(scores[row])):
total += scores[row][col]
print(f"Student {row + 1}: total {total}")Student 1: total 252
Student 2: total 213
total = 0 is inside the outer loop, so it starts again for each student, and print is at the outer loop’s level, so it runs once per row, after the inner loop: the same placement rules as Day 5’s dice_sums.py. Each row is a list of numbers, so sum(scores[row]) would give the same totals in one step; the nested loops show how scores[row][col] reaches every cell.
Key idea
A grid is a list of rows, each row a list. grid[row][col] reads or changes one cell; len(grid) counts the rows and
len(grid[0]) the columns.
Watch out
Repeating a row with *. [0] * 5 made five zeros in 6.1, so [["."] * 3] * 3 looks like a quick 3 × 3 board:
board = [["."] * 3] * 3
board[0][0] = "X"
for row in board:
print(" ".join(row))X . .
X . .
X . .One assignment put an X in all three rows. * 3 repeats the items of the outer list, and its one item is a reference to one row list. The result is a list holding the same row three times: three aliases (6.9). Changing “row 0” changes that one shared row, and the other two “rows” are the same object. Build a separate row on each pass instead:
board = []
for row in range(3):
board.append(["."] * 3)
board[0][0] = "X"
for row in board:
print(" ".join(row))X . .
. . .
. . .["."] * 3 runs once per pass, so each append adds a new, separate row. (* was fine inside each row, because its items are strings, which cannot change.)
Pause and try
Question 13 · Tracing
What does this program print?
grid = [[1, 2], [3, 4], [5, 6]]
grid[2][0] = grid[0][1] + grid[1][1]
column_total = 0
for row in grid:
column_total += row[0]
print(grid[-1])
print(column_total)Check your reasoning
[6, 6]
10Line 2 works out the right side first: grid[0][1] is row 0, column 1, which is 2, and grid[1][1] is 4, so it stores 6 in row 2, column 0. The grid is now [[1, 2], [3, 4], [6, 6]].
The loop goes through the rows, and row[0] is each row’s first item: 1, 3, then 6. column_total is 1 + 3 + 6 = 10. grid[-1] is the last row, [6, 6].
Reread 6.10 Nested Lists as a Grid if your output differed.
Putting it together
A shopping list program keeps asking for a command until the user types done. add asks for an item and adds it unless it is already there, remove takes an item off if it is there, and show prints the list in alphabetical order with numbers. This run adds milk, bread, Milk (again, with a capital), and apples, shows the list, tries to remove eggs, removes milk, and finishes:
shopping = []
while True:
command = input("Command (add, remove, show, done): ").strip().lower()
if command == "done":
break
if command == "add":
item = input("Item: ").strip().lower()
if item in shopping:
print(f"{item} is already on the list.")
else:
shopping.append(item)
elif command == "remove":
item = input("Item: ").strip().lower()
if item in shopping:
shopping.remove(item)
else:
print(f"{item} is not on the list.")
elif command == "show":
for number, item in enumerate(sorted(shopping), 1):
print(f"{number}. {item}")
else:
print("Unknown command.")
print(f"{len(shopping)} items: {', '.join(sorted(shopping))}")Command (add, remove, show, done): add
Item: milk
Command (add, remove, show, done): add
Item: bread
Command (add, remove, show, done): add
Item: Milk
milk is already on the list.
Command (add, remove, show, done): add
Item: apples
Command (add, remove, show, done): show
1. apples
2. bread
3. milk
Command (add, remove, show, done): remove
Item: eggs
eggs is not on the list.
Command (add, remove, show, done): remove
Item: milk
Command (add, remove, show, done): done
2 items: apples, bread
| Typed | What happens | shopping after |
Idea |
|---|---|---|---|
add, milk |
Not in the list: append |
['milk'] |
6.4, 6.7 |
add, bread |
Not in the list: append |
['milk', 'bread'] |
6.4 |
add, Milk |
lower() makes it milk, which is in the list |
['milk', 'bread'] |
6.7 |
add, apples |
append |
['milk', 'bread', 'apples'] |
6.4 |
show |
enumerate(sorted(shopping), 1) numbers a sorted copy |
unchanged | 6.6, 6.8 |
remove, eggs |
Not in the list: message, no ValueError |
unchanged | 6.5 |
remove, milk |
remove |
['bread', 'apples'] |
6.5 |
done |
break |
unchanged | Day 5 |
- Every item is lowercased (Day 3) before it is checked or stored, so
Milkandmilkcount as the same item, andin(which compares whole items exactly) catches the duplicate. showusessorted, notsort, so the list keeps the order items were added in; only the display is alphabetical.
Day 12 builds a to-do list manager with the same shape: a list that a menu loop adds to, shows, and removes from.
Vocabulary
| Term | Definition | Example |
|---|---|---|
| list | One value holding several values in order, in square brackets | [88, 92, 79] |
| item / element | One value inside a list | 92 |
| empty list | A list with no items | [] |
| sequence | A collection of items kept in order at numbered positions | a string or a list |
| mutable / immutable | Can / cannot be changed after it is made | lists / strings |
| item assignment | Replacing the item at one position | scores[0] = 90 |
append() |
Adds one item to the end of a list | names.append("Ana") |
insert() |
Adds one item at an index, shifting later items right | names.insert(0, "Dev") |
extend() |
Adds each item of another sequence to the end | a.extend([4, 5]) |
pop() |
Removes and gives back the last item, or the item at an index | queue.pop(0) |
remove() |
Removes the first item equal to a value | cart.remove("milk") |
clear() |
Removes every item | tasks.clear() |
| built-in function | A function that is always available | len, min, max, sum |
not in |
True when no item equals the value |
100 not in scores |
| sorting | Putting items in order | [45, 72, 88] |
sort() |
Sorts a list in place and gives back None |
scores.sort() |
sorted() |
Gives back a new sorted list, leaving the original alone | sorted(scores) |
| keyword argument | An argument passed with a name and = |
reverse=True |
reverse() |
Flips a list’s order in place, without sorting | stops.reverse() |
| alias / aliasing | Two names for the same object | b = a |
| copy | A new, separate list with the same items | a.copy(), a[:], list(a) |
| nested list | A list that is an item of another list | [1, [2, 3]] |
| grid / 2D list | A list of rows, each row a list | [[1, 2], [3, 4]] |
Summary
- A list holds several items in order under one name. It indexes and slices exactly like a string:
items[0]is the first item,items[-1]the last, and a slice is a new list. - Lists are mutable:
items[i] = valuereplaces an item,append,insert, andextendadd items, andpop,remove, andcleartake them out. These methods change the list and give backNone(exceptpop, which gives back the removed item), so never assign their result. - Loop over a list with
for item in items:; build a new list to keep some items, and loop overrange(len(items))to change items in place.len,in,min,max, andsumsummarize a list without writing a loop. items.sort()sorts the list itself;sorted(items)gives back a new sorted list and leaves the original alone.b = amakes an alias, a second name for the same list, so changing one changes “the other”. Usea.copy()for a separate list. A grid is a list of lists, read withgrid[row][col].
Practice
Do these in your day06 folder. Type each program yourself, run it, and only then open the answer.
Question 14 · Short answer
Min, max, and average. Ask the user for five numbers, one at a time, and store them in a list. Then report the smallest, the largest, and the average with two decimal places.
Check your reasoning
One possible solution, with a run that types 12, 7.5, 20, 3, and 9:
numbers = []
for count in range(1, 6):
number = float(input(f"Number {count}: "))
numbers.append(number)
average = sum(numbers) / len(numbers)
print("Min:", min(numbers))
print("Max:", max(numbers))
print(f"Average: {average:.2f}")Number 1: 12
Number 2: 7.5
Number 3: 20
Number 4: 3
Number 5: 9
Min: 3.0
Max: 20.0
Average: 10.30The list starts empty before the loop, and each pass converts the typed string with float() (so 7.5 works) and appends it. After the loop, min, max, and sum(numbers) / len(numbers) do the work. Converting before appending matters: a list of strings would compare as text, and sum cannot add strings. The total is 51.5, and 51.5 / 5 is 10.3, shown as 10.30 by :.2f.
Reread 6.4 Adding Items: append, insert, extend and 6.7 len, in, min, max, and sum if your result differed.
Question 15 · Short answer
Remove duplicates. Start with a list that has repeated items, such as ["apple", "pear", "apple", "fig", "pear", "kiwi"]. Make a list with each item only once, keeping the order in which each item first appeared.
Check your reasoning
One possible solution:
items = ["apple", "pear", "apple", "fig", "pear", "kiwi"]
unique = []
for item in items:
if item not in unique:
unique.append(item)
print(f"Before: {items}")
print(f"After: {unique}")Before: ['apple', 'pear', 'apple', 'fig', 'pear', 'kiwi']
After: ['apple', 'pear', 'fig', 'kiwi']The loop goes through the items in order and appends an item to unique only if it is not in unique yet. The first apple and the first pear are added; their second copies are already in unique, so they are skipped. Because items are added in the order they are first met, the original order is kept. Building a new list, rather than removing duplicates from items while looping over it, avoids the skipping problem from 6.6. Sorting the list first would also group duplicates, but would lose the original order.
Reread 6.6 Looping Over a List if your result differed.
Question 16 · Short answer
The aliasing bug. Write a program that keeps a list of regular prices and makes a list of sale prices at 20% off (multiply by 0.8). First write it with sale_prices = prices and show that the regular prices change too. Then fix it.
Check your reasoning
The bug, on purpose:
prices = [4.0, 10.0, 7.5]
sale_prices = prices
for i in range(len(sale_prices)):
sale_prices[i] = sale_prices[i] * 0.8
print("Regular:", prices)
print("Sale:", sale_prices)Regular: [3.2, 8.0, 6.0]
Sale: [3.2, 8.0, 6.0]sale_prices = prices makes an alias: one list with two names. Changing sale_prices[i] changes that one list, so the regular prices are gone. The fix copies the list first:
prices = [4.0, 10.0, 7.5]
sale_prices = prices.copy()
for i in range(len(sale_prices)):
sale_prices[i] = sale_prices[i] * 0.8
print("Regular:", prices)
print("Sale:", sale_prices)Regular: [4.0, 10.0, 7.5]
Sale: [3.2, 8.0, 6.0]Only line 2 changed. Now sale_prices is a separate list, so the loop’s item assignments change only the copy. prices[:] or list(prices) would work just as well.
Reread 6.9 Aliasing Versus Copying if your result differed.
Review questions
Question 17 · Tracing
What does this program print? Track the list after each line.
line = ["Ana", "Ben", "Cy"]
line.extend(["Dot", "Eve"])
first = line.pop(0)
line.insert(1, first)
print(line[1:3])
print(line[-1], len(line))Check your reasoning
['Ana', 'Cy']
Eve 5| Line | Action | line after |
|---|---|---|
| 2 | extend two names |
['Ana', 'Ben', 'Cy', 'Dot', 'Eve'] |
| 3 | pop(0) gives 'Ana' |
['Ben', 'Cy', 'Dot', 'Eve'] |
| 4 | insert(1, 'Ana') |
['Ben', 'Ana', 'Cy', 'Dot', 'Eve'] |
line[1:3] is indexes 1 and 2: ['Ana', 'Cy']. The last item is 'Eve', and popping one and inserting one leaves 5 items.
Reread 6.5 Removing Items: pop, remove, clear and 6.2 Indexing and Slicing Lists if your output differed.
Question 18 · Debugging
This program should print the top score. It stops with an error. Quote the last line of the error, explain it, and write the fix.
scores = [72, 91, 88]
ranked = scores.sort(reverse=True)
print("Top score:", ranked[0])Check your reasoning
TypeError: 'NoneType' object is not subscriptablescores.sort(reverse=True) sorts scores in place and gives back None, so ranked is None. NoneType is the type of None, and subscriptable means “can be indexed with square brackets”. None holds no items, so ranked[0] is a TypeError. Use sorted, which gives back a new list:
scores = [72, 91, 88]
ranked = sorted(scores, reverse=True)
print("Top score:", ranked[0])Top score: 91scores.sort(reverse=True) on its own line followed by scores[0] also works, but changes the order of scores. And max(scores) gives the top score with no sorting at all.
Reread the Watch Out in 6.8 Sorting and Reversing if you missed it.
Question 19 · Tracing
What does this program print? Decide, line by line, whether each line changes a list or makes a new one.
a = [1, 2]
b = a
a = a + [3]
b.append(4)
print(a)
print(b)Check your reasoning
[1, 2, 3]
[1, 2, 4]| Line | What happens | a |
b |
|---|---|---|---|
| 2 | b becomes an alias of the list [1, 2] |
[1, 2] |
[1, 2] (same list) |
| 3 | a + [3] builds a new list; a is rebound |
[1, 2, 3] |
[1, 2] |
| 4 | append changes the list b refers to |
[1, 2, 3] |
[1, 2, 4] |
After line 3, a and b refer to different lists, so the append on line 4 reaches only b’s list. If line 3 had been a.append(3), both names would still share one list, and both would print [1, 2, 3, 4].
Reread 6.9 Aliasing Versus Copying if your output differed.
Question 20 · Multiple choice
scores holds some test scores. Which code leaves passing holding exactly the scores of 60 or more, with none missed?
for score in scores:withif score < 60: scores.remove(score)inside, thenpassing = scorespassing = [], thenfor score in scores:withif score >= 60: passing.append(score)insidepassing = scores.remove(score < 60)passing = [], thenfor score in scores:withpassing = [score]inside whenscore >= 60
Check your reasoning
B. It builds a new list, appending each score that passes, and never changes the list it is looping over.
- A is wrong: removing items from the list the for loop is going through shifts the later items left, so the item after each removed one is skipped, and a failing score can survive (6.6). It also changes
scores, andpassingis only an alias of it. - C is wrong:
score < 60is abool, soremovelooks for an item equal toTrueorFalse, not for low scores. Andremovegives backNone, sopassingwould beNone. - D is wrong:
passing = [score]replaces the whole list with a one-item list on every passing score, so only the last passing score is left. Adding to a list isappend.
Reread 6.6 Looping Over a List if you chose differently.
Question 21 · Tracing
What does this program print? Think about what copy() copies when the items are lists.
grid = [[1, 2], [3, 4]]
backup = grid.copy()
grid[0][0] = 99
grid.append([5, 6])
print(backup)
print(len(grid), len(backup))Check your reasoning
[[99, 2], [3, 4]]
3 2grid.copy() builds a new outer list, so backup is grid is False. But a copy holds the same items as the original (6.9), and here the items are the two row lists. Both outer lists refer to the same rows:
| Change | Reaches backup? |
Why |
|---|---|---|
grid[0][0] = 99 |
Yes | It changes row 0, a list that both outer lists share |
grid.append([5, 6]) |
No | It changes grid’s outer list, and backup has its own |
So backup shows the 99, but still has only 2 rows, while grid has 3. To copy a grid completely, copy each row too: start with an empty list and append(row.copy()) for every row.
Reread 6.9 Aliasing Versus Copying and 6.10 Nested Lists as a Grid if your output differed.
Question 22 · Short answer
A program reads three ages with input() and appends each one to ages without converting it. The user types 9, 10, and 100. Then it prints sorted(ages) and gets ['10', '100', '9']. Explain why the ages are in this order, and say what to change so they sort as numbers.
Check your reasoning
Model answer: input() always gives back a string (Day 3), so the list holds the strings '9', '10', and '100', not numbers. Strings are sorted as text, comparing character by character from the left. '1' comes before '9', so both strings that start with 1 come first, and '10' comes before '100' because it is shorter and otherwise the same. Convert each age before appending it, with ages.append(int(input("Age: "))); then sorted(ages) gives [9, 10, 100].
A correct answer says that the items are strings, that strings sort character by character, and that converting with int() before appending fixes it.
Reread 6.8 Sorting and Reversing if your reasoning differed.
Go further
The official Python Tutorial introduces lists in An Informal Introduction to Python: Lists and lists every list method in Data Structures: More on Lists. Its examples also use tuples and list comprehensions, which you meet on Day 7 and Day 11.
Reflection
Check each statement. If the answer is “not yet,” reread the linked subsection.
- I can make a list, including an empty one, and count its items with len(). Reread
- I can read any item or slice of a list, including with negative indexes. Reread
- I can change an item in place and explain why that fails for a string. Reread
- I can add items with append, insert, and extend, and explain how append and extend differ. Reread
- I can remove items with pop, remove, and clear without causing an error. Reread
- I can loop over a list, change its items by index, and build a new list of the items to keep. Reread
- I can summarize a list with len, in, min, max, and sum. Reread
- I can choose between sort() and sorted() and explain the difference. Reread
- I can explain why changing one list can change "another" one, and make a real copy. Reread
- I can read and change any cell of a grid with two indexes. Reread