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
| Method | Does | Changes the list? |
|---|---|---|
.append(x) | Add one item at the end | Yes |
.extend(other) | Add all of another list's items | Yes |
.insert(i, x) | Add at position i | Yes |
.remove(x) | Delete the first x. Raises ValueError if absent | Yes |
.pop() / .pop(i) | Remove and return the last, or the i-th | Yes |
.sort() | Sort in place | Yes, and returns None |
.reverse() | Reverse in place | Yes |
.clear() | Empty it | Yes |
.count(x) | How many x | No |
.index(x) | Position of the first x | No |
sorted(list) | A new sorted list | No |
list.copy() | A shallow copy | No |
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
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).
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. swordStatistics 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: 73That 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.
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.