Lists — many values under one name
Holding as many values as you like under one name, reaching inside with an index and a slice, the methods that change a list, and the trap of two names ending up attached to the same one.
- 1Encounter
- 2Understand
- 3Worked
- 4Predict
- 5Apply
- 6Stretch
The problem we are solving
A shop needs to hold the prices of three products. With everything we have learned so far there is only one way to do it:
price_1 = 15
price_2 = 60
price_3 = 850
print(price_1 + price_2 + price_3)925Now the question: what about forty products? Forty variables. And a line of arithmetic with forty pieces in it. And when the shop starts selling something new, you have to go into the code and invent another variable.
The problem is not only the typing, it is the shape. To Python, price_1, price_2 and price_3 are three completely unrelated names — as unrelated as name and age. You know they are three instances of the same thing. We simply have no way yet of writing "many of the same thing" in code.
A list is that way. One name, as many values as you like underneath it, in order.
By the end of this chapter you can
- Build a list and reach inside it with an index and a slice
- Change a list with
append,insert,removeandpop - Explain why a list can be changed when a piece of text cannot
- Recognise and avoid
b = aleaving two names attached to one list - Read an
IndexErrorand aValueErrorand tell what went wrong
Prerequisites: for loops and range.
Making a list
Square brackets, with the values separated by commas:
prices = [15, 60, 850]
print(prices)
print(len(prices))[15, 60, 850]
3len() works here exactly as it did on text — three values, so 3.
Printing shows the whole list, brackets and all. That is not for a user to read, it is for you to understand: [15, 60, 850] tells you this is a list, not 925.
Reaching inside
Indexing works exactly as it did for strings — counting from zero, negative from the back:
prices = [15, 60, 850]
print(prices[0])
print(prices[-1])
print(prices[1:])15
850
[60, 850]All three lines are chapter six's rules, unchanged. One difference is worth noticing: prices[0] gives back a number, while prices[1:] gives back a list. A slice always returns the same kind of thing it was cut from, never a single piece.
And here is the real difference: a list can be changed
In chapter six we saw that a character inside text cannot be swapped out:
name = "rafi"
name[0] = "R"TypeError: 'str' object does not support item assignmentThe identical line works on a list:
prices = [15, 60, 850]
prices[0] = 20
print(prices)[20, 60, 850]No new list was made — this list changed. That is the most important property a list has, and the trap at the end of this chapter comes straight out of it.
The words for this: text is immutable, a list is mutable.
Adding and taking away
stock = ["pen", "notebook"]
stock.append("bag")
stock.insert(1, "eraser")
print(stock)
stock.remove("pen")
print(stock)
last = stock.pop()
print(last)
print(stock)['pen', 'eraser', 'notebook', 'bag']
['eraser', 'notebook', 'bag']
bag
['eraser', 'notebook']Four methods, four different jobs:
append(x)— adds at the endinsert(i, x)— puts it at positioni, shifting everything after it rightremove(x)— finds the first item with that value and drops itpop()— drops the last item and hands it back
The difference between those last two is where most of the confusion lives: remove wants to know what to drop, pop wants to know from where. And pop gives you the thing it removed; remove does not.
These methods do not return the list. With text you had to keep the result ofname.upper(), because it built new text and handed it back. Lists are the other way round:appendchanges the list itself and returnsNone.
Which makes this a classic mistake:
stock = ["pen", "notebook"]
stock = stock.append("bag")
print(stock)NoneThe list is gone. Write stock.append("bag"), not stock = stock.append("bag").
Searching and walking
stock = ["pen", "notebook", "bag"]
print("pen" in stock)
print("chair" in stock)
for item in stock:
print(item.upper())True
False
PEN
NOTEBOOK
BAGin searched inside text, and it searches a list too — the same word doing the same job.
And for item in stock walks the values one at a time. Notice there is no range and no index arithmetic, exactly as when we walked the characters of a word in chapter eleven.
When you need the position and the value, enumerate:
stock = ["pen", "notebook"]
for position, item in enumerate(stock, start=1):
print(position, item)1 pen
2 notebookWithout start=1 the count begins at zero. A list being shown to a person usually wants to start at one.
The trap that catches everyone
In chapter four we saw this:
a = 3
b = a
b = b + 1
print(a, b)3 4b received a value and was not watching a. Now do the same thing with a list:
a = [1, 2, 3]
b = a
b.append(4)
print(a)
print(b)[1, 2, 3, 4]
[1, 2, 3, 4]b was changed, and a changed too.
The reason does not contradict the earlier rule, it is the earlier rule. On the line b = a, b received exactly what a held — and what a held was where the list is, not a copy of the list. So both names now point at the same list. b.append(4) changed that one list, and both names show the same change.
You never see this with numbers, because b = b + 1 does not modify the old number — it makes a new one and puts it in b. Numbers are safe precisely because they cannot be changed. Lists can, so they are not.
If you want a separate copy, you have to ask for one:
a = [1, 2, 3]
b = a.copy()
b.append(4)
print(a)
print(b)[1, 2, 3]
[1, 2, 3, 4]When to be careful. Any time you put a list under a second name, or pass a list into a function (chapter nineteen), ask yourself once: do I mean to change this one, or do I want a copy? The people who never ask that question spend days hunting bugs that read as "but I did not change anything here".
A complete example
shopping.py:
# A shopping list, built up and then summarised
items = []
items.append("pen")
items.append("notebook")
items.append("bag")
items.insert(0, "eraser")
print("List :", items)
print("Count :", len(items))
print("First :", items[0])
print("Last :", items[-1])
print("Has bag :", "bag" in items)
items.remove("pen")
dropped = items.pop()
print()
print("After removing 'pen' and popping the last one:")
print("List :", items)
print("Dropped :", dropped)
print("Count :", len(items))
print()
print("Numbered:")
for position, item in enumerate(items, start=1):
print(f" {position}. {item}")List : ['eraser', 'pen', 'notebook', 'bag']
Count : 4
First : eraser
Last : bag
Has bag : True
After removing 'pen' and popping the last one:
List : ['eraser', 'notebook']
Dropped : bag
Count : 2
Numbered:
1. eraser
2. notebookThree things worth noticing.
It starts from an empty list. The line items = [] is doing the same job an accumulator did outside a loop in chapter ten — where total = 0 was, [] is now. You need somewhere to collect into before you start collecting.
insert(0, "eraser") put it at the front, so eraser is first in the printed list. The order things were added is not the order they end up in.
pop() handed back what it removed, so dropped holds bag. remove("pen") returned nothing, so the removed pen is simply gone.
When it breaks
IndexError: list index out of range An index beyond the number of values in the list. In a list of three the valid indexes are 0, 1, 2 — there is no prices[3]. For the last value, prices[-1] is the safest thing to write, because it needs no knowledge of the length.
ValueError: list.remove(x): x not in list The value you asked to remove is not there. Check with if x in stock: first, or use try from chapter twenty-four.
AttributeError: 'NoneType' object has no attribute 'append' Some earlier line said stock = stock.append(...), so stock is now None. Do not keep the result — stock.append(...) on its own is the whole operation.
I changed one list and another one changed too Two names are attached to one list. Write b = a.copy() rather than b = a.
I am removing inside a loop and things are being skipped
numbers = [1, 2, 2, 3, 4]
for n in numbers:
if n % 2 == 0:
numbers.remove(n)
print(numbers)[1, 2, 3]One 2 survived. Shortening the list you are walking puts Python's counting out of step with the list's positions, so some values get stepped over. The fix is not to remove but to collect what you want to keep into a new list.
numbers = [1, 2, 2, 3, 4]
kept = []
for n in numbers:
if n % 2 != 0:
kept.append(n)
print(kept)[1, 3]I named a variable list and now list(...) does not work list is one of Python's own names. Use a meaningful one — items, prices, names.
Step 4 of 6 — Predict
Check your understanding
The result of append is stored back into stock. What is printed?
stock = ["pen", "notebook"]
stock = stock.append("bag")
print(stock)- ANone
- B['pen', 'notebook', 'bag']
- C['bag']
- DAn `AttributeError`
Only b was changed. What do the two lines print?
a = [1, 2, 3]
b = a
b.append(4)
print(a)
print(b)- A[1, 2, 3, 4] [1, 2, 3, 4]
- B[1, 2, 3] [1, 2, 3, 4]
- C[1, 2, 3, 4] [1, 2, 3]
- D[1, 2, 3] [4]
The list holds three values and index three is asked for. What happens?
prices = [15, 60, 850]
print(prices[3])- AAn `IndexError` — with three values the valid indexes are `0`, `1`, `2`
- B850 — the last value
- CNone
- DAn empty list
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
Your turn
Write a file called marks.py holding a list of at least six marks.
Then print:
- The whole list, and how many marks are in it
- The first and the last mark — use
[-1]for the last - The total and the average, accumulated with a loop rather than typed in by hand
- A numbered list using
enumerate, starting at one - A new list of the passing marks (40 or above), and how many there are
Then run two experiments, and both matter:
appendanother mark to the end and print the total again. Did you have to change the line that calculates it? If not, you wrote a real calculation.- Write
backup = marks, thenmarks.append(100), and print both. What is inbackup? Now do the same thing with.copy()and watch the difference.
That last experiment is the most valuable three minutes in this chapter. Seeing it once with your own eyes means never spending an afternoon on it later.
Step 6 of 6
Stretch — the chapter quiz
Ten questions from easy to hard. The last ones are difficult on purpose.
Sign in to take the quiz