Chapter 12

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.

30 minPython 3.12
  1. 1Encounter
  2. 2Understand
  3. 3Worked
  4. 4Predict
  5. 5Apply
  6. 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:

python
price_1 = 15
price_2 = 60
price_3 = 850

print(price_1 + price_2 + price_3)
text
925

Now 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, remove and pop
  • Explain why a list can be changed when a piece of text cannot
  • Recognise and avoid b = a leaving two names attached to one list
  • Read an IndexError and a ValueError and tell what went wrong

Prerequisites: for loops and range.


Making a list

Square brackets, with the values separated by commas:

python
prices = [15, 60, 850]

print(prices)
print(len(prices))
text
[15, 60, 850]
3

len() 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:

python
prices = [15, 60, 850]

print(prices[0])
print(prices[-1])
print(prices[1:])
text
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:

python
name = "rafi"
name[0] = "R"
text
TypeError: 'str' object does not support item assignment

The identical line works on a list:

python
prices = [15, 60, 850]
prices[0] = 20
print(prices)
text
[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

python
stock = ["pen", "notebook"]

stock.append("bag")
stock.insert(1, "eraser")
print(stock)

stock.remove("pen")
print(stock)

last = stock.pop()
print(last)
print(stock)
text
['pen', 'eraser', 'notebook', 'bag']
['eraser', 'notebook', 'bag']
bag
['eraser', 'notebook']

Four methods, four different jobs:

  • append(x) — adds at the end
  • insert(i, x) — puts it at position i, shifting everything after it right
  • remove(x) — finds the first item with that value and drops it
  • pop() — 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 of name.upper(), because it built new text and handed it back. Lists are the other way round: append changes the list itself and returns None.

Which makes this a classic mistake:

python
stock = ["pen", "notebook"]
stock = stock.append("bag")
print(stock)
text
None

The list is gone. Write stock.append("bag"), not stock = stock.append("bag").

Searching and walking

python
stock = ["pen", "notebook", "bag"]

print("pen" in stock)
print("chair" in stock)

for item in stock:
    print(item.upper())
text
True
False
PEN
NOTEBOOK
BAG

in 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:

python
stock = ["pen", "notebook"]
for position, item in enumerate(stock, start=1):
    print(position, item)
text
1 pen
2 notebook

Without 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:

python
a = 3
b = a
b = b + 1
print(a, b)
text
3 4

b received a value and was not watching a. Now do the same thing with a list:

python
a = [1, 2, 3]
b = a
b.append(4)

print(a)
print(b)
text
[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:

python
a = [1, 2, 3]
b = a.copy()
b.append(4)

print(a)
print(b)
text
[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:

python
# 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}")
text
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. notebook

Three 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

python
numbers = [1, 2, 2, 3, 4]

for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)

print(numbers)
text
[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.

python
numbers = [1, 2, 2, 3, 4]
kept = []

for n in numbers:
    if n % 2 != 0:
        kept.append(n)

print(kept)
text
[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.