Level 2 · The Toolbox

Lists: Many Things at Once 📋

Up to now every variable has held exactly one thing. That is a very small world. A list holds as many things as you like, in order, and it changes everything.

A list is things in a row

inventory = ["rubber chicken", "map", "grog", "sword"]

print(inventory)
print(len(inventory))
print(inventory[0])
print(inventory[-1])
['rubber chicken', 'map', 'grog', 'sword']
4
rubber chicken
sword

Square brackets, commas between items. Positions count from zero, negatives count from the right, and slicing works exactly as it did for strings, because both are sequences.

letters = ["a", "b", "c", "d", "e", "f"]

print(letters[1:4])
print(letters[:3])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])
['b', 'c', 'd']
['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']

Unlike strings, lists can be changed

crew = ["Guybrush", "Elaine", "Otis"]

crew[2] = "Meathook"            # replace in place
print(crew)

crew.append("Carla")            # add one to the end
print(crew)

crew.insert(0, "LeChuck")       # add at a position
print(crew)

crew.remove("LeChuck")          # remove by value (the first match)
print(crew)

gone = crew.pop()               # remove the last, and give it back
print(gone, crew)

del crew[0]                     # remove by position
print(crew)
['Guybrush', 'Elaine', 'Meathook']
['Guybrush', 'Elaine', 'Meathook', 'Carla']
['LeChuck', 'Guybrush', 'Elaine', 'Meathook', 'Carla']
['Guybrush', 'Elaine', 'Meathook', 'Carla']
Carla ['Guybrush', 'Elaine', 'Meathook']
['Elaine', 'Meathook']

This is the crucial difference from strings. Strings are immutable: every operation returns a new string. Lists are mutable: operations change the list you already have. Almost every surprise in this lesson comes from that one fact.

The methods worth knowing

MethodDoesChanges the list?
.append(x)Add one item at the endYes
.extend(other)Add all of another list's itemsYes
.insert(i, x)Add at position iYes
.remove(x)Delete the first x. Raises ValueError if absentYes
.pop() / .pop(i)Remove and return the last, or the i-thYes
.sort()Sort in placeYes, and returns None
.reverse()Reverse in placeYes
.clear()Empty itYes
.count(x)How many xNo
.index(x)Position of the first xNo
sorted(list)A new sorted listNo
list.copy()A shallow copyNo
🪤 sort() returns None

names = names.sort() is the classic disaster: .sort() sorts the list and returns None, so you have just thrown your list away and replaced it with nothing. Either names.sort() on its own line, or names = sorted(names). Never both.

scores = [88, 42, 95, 61]

new_list = sorted(scores)
print(scores, "unchanged")
print(new_list, "the sorted copy")

scores.sort()
print(scores, "now sorted in place")

scores.sort(reverse=True)
print(scores, "descending")
[88, 42, 95, 61] unchanged
[42, 61, 88, 95] the sorted copy
[42, 61, 88, 95] now sorted in place
[95, 88, 61, 42] descending

Sorting by something other than the value

crew = ["Guybrush", "Otis", "Elaine", "Meathook"]

print(sorted(crew))                    # alphabetical
print(sorted(crew, key=len))           # by length
print(sorted(crew, key=str.lower))     # case-insensitive
print(max(crew, key=len))
['Elaine', 'Guybrush', 'Meathook', 'Otis']
['Otis', 'Elaine', 'Guybrush', 'Meathook']
['Elaine', 'Guybrush', 'Meathook', 'Otis']
Guybrush

key= takes a function and sorts by what that function returns. It is one of the most useful arguments in the whole language, and it comes back constantly once you have data with structure. Note Guybrush and Meathook are both eight letters, and Python kept them in their original relative order: Python's sort is stable, which is a guarantee you can rely on.

Searching and testing

inventory = ["map", "grog", "sword"]

print("grog" in inventory)
print("banana" in inventory)
print(inventory.index("grog"))
print(inventory.count("grog"))
True
False
1
1

The trap: two names, one list

original = ["a", "b", "c"]
copy = original          # this is NOT a copy

copy.append("d")

print("original:", original)
print("copy:    ", copy)
print("same object?", original is copy)
original: ['a', 'b', 'c', 'd']
copy:     ['a', 'b', 'c', 'd']
same object? True
PERCEPTION[Formidable: Success]

Remember Lesson 2, where b = a copied the value and the two went their separate ways? That was true for a number. It is not true here.

A variable does not hold a list. It holds an arrow pointing at a list. copy = original draws a second arrow at the same list. There is one list and two names for it, and changing it through either name changes it for both.

Three ways to make a genuine copy:

original = ["a", "b", "c"]

copy1 = original.copy()
copy2 = list(original)
copy3 = original[:]

copy1.append("changed")
print(original)
print(copy1)
['a', 'b', 'c']
['a', 'b', 'c', 'changed']

All three are equivalent for a flat list. If your list contains other lists, these are shallow copies: the outer list is new, the inner lists are still shared. For that, copy.deepcopy(), which Lesson 15 covers.

Building a list up

squares = []

for n in range(1, 6):
    squares.append(n * n)

print(squares)
print(sum(squares), min(squares), max(squares))
[1, 4, 9, 16, 25]
55 1 25

Start empty, append in a loop, use afterwards. This is the most common shape in beginner Python, and in Lesson 16 you will learn to write it in one line with a comprehension. Learn it this way first; the short version means nothing if you cannot see the loop inside it.

Lists of anything, including lists

mixed = [42, "grog", 3.5, True, None]
print(mixed)

grid = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]
print(grid[1])
print(grid[1][2])

for row in grid:
    print(" ".join(str(n) for n in row))
[42, 'grog', 3.5, True, None]
[4, 5, 6]
6
1 2 3
4 5 6
7 8 9

A list can hold anything, including other lists. grid[1][2] reads as "row 1, then item 2 of that". Mixed-type lists are legal but usually a smell: if the items mean different things, you probably want a dictionary (Lesson 13) or a class (Lesson 31).

Exercise 1

Inventory manager

Start with three items. Add two, remove one by name, sort what remains, and print a numbered list.

Reveal solution
inventory = ["sword", "map", "grog"]

inventory.append("rubber chicken")
inventory.append("mints")
inventory.remove("grog")
inventory.sort()

print(f"You are carrying {len(inventory)} things:")
for i, item in enumerate(inventory, start=1):
    print(f"  {i}. {item}")
You are carrying 4 things:
  1. map
  2. mints
  3. rubber chicken
  4. sword
Exercise 2

Statistics without a library

For the list [88, 42, 95, 61, 73], print the highest, lowest, total, average to one decimal place, and the median.

Reveal solution
scores = [88, 42, 95, 61, 73]

print(f"Highest: {max(scores)}")
print(f"Lowest:  {min(scores)}")
print(f"Total:   {sum(scores)}")
print(f"Average: {sum(scores) / len(scores):.1f}")

ordered = sorted(scores)
middle = len(ordered) // 2
print(f"Median:  {ordered[middle]}")
Highest: 95
Lowest:  42
Total:   359
Average: 71.8
Median:  73

That median is only correct for an odd number of values. For an even count you must average the middle two, which is exactly the sort of edge case that makes people reach for the standard library: import statistics; statistics.median(scores) handles both.

Exercise 3

Predict the aliasing

What does this print? Think carefully.

a = [1, 2, 3]
b = a
c = a.copy()

b.append(4)
c.append(5)

print(a)
print(b)
print(c)
Reveal solution

[1, 2, 3, 4], then [1, 2, 3, 4], then [1, 2, 3, 5].

b is another name for a, so appending through b shows up in a. c is a real copy, so it goes its own way.

If you got this right, you understand the single most important thing in this lesson, and you have avoided a bug that costs professionals real hours.

+100 XP