Lesson 5 — Lists

By the end of this you can: keep many values in one name, reach any of them by number, add and remove items while the program runs, ask whether something is in there, and loop over the whole thing.

The problem lists solve

You already know how to hold one thing:

enemy = "ghosty"
print(enemy)
ghosty

Now hold three. The obvious way is three variables:

enemy1 = "ghosty"
enemy2 = "bobo"
enemy3 = "gigagantrum"
print(enemy1, enemy2, enemy3)
ghosty bobo gigagantrum

That works right up until it doesn't. How do you spawn all of them? Three lines. How about twenty enemies? Twenty variables and twenty lines. And you cannot loop over them, because enemy1 and enemy2 are unrelated names that happen to look similar — Python sees no connection at all.

A list is one name holding many values, in order:

enemies = ["ghosty", "bobo", "gigagantrum"]
print(enemies)
print(len(enemies))
['ghosty', 'bobo', 'gigagantrum']
3

Square brackets, commas between the items. len() tells you how many are in there.

Reaching one item

Each item has a position number, and you get at it with square brackets:

enemies = ["ghosty", "bobo", "gigagantrum"]

print(enemies[0])
print(enemies[1])
print(enemies[2])
ghosty
bobo
gigagantrum

Counting starts at 0. The first item is enemies[0], not enemies[1]. This feels wrong for about a week and then never bothers you again. It is the same 0 that range(5) starts at, which is not a coincidence.

So the last item of a three-item list is at index 2 — one less than the length. Asking for enemies[3] does not give you nothing; it stops the program with IndexError: list index out of range.

There is a shortcut for counting from the end:

enemies = ["ghosty", "bobo", "gigagantrum"]

print(enemies[-1])
print(enemies[-2])
gigagantrum
bobo

-1 is the last item, whatever the length is. That is worth knowing, because enemies[len(enemies) - 1] is the same thing written the long way.

Changing a list while it runs

A list is not fixed once you make it. This is the part that makes it useful in a game:

bag = ["coin", "key"]

bag.append("bomb")
print(bag)

bag.remove("key")
print(bag)

print(len(bag))
['coin', 'key', 'bomb']
['coin', 'bomb']
2
  • append() adds one item to the end.
  • remove() takes out the first item matching what you name.
  • len() keeps up on its own — you never maintain a count yourself.

You can also change an item in place, by assigning to its position:

bag = ["coin", "key", "bomb"]
bag[1] = "lamp"
print(bag)
['coin', 'lamp', 'bomb']

remove() on something that is not there is an error, so it is usually paired with a check — which is the next thing.

Asking what is in there

in answers a yes-or-no question about a list, and gives you back True or False — so it goes straight into an if:

bag = ["coin", "key"]

print("key" in bag)
print("bomb" in bag)

if "key" in bag:
    print("The door opens")
else:
    print("The door is locked")
True
False
The door opens

This is the safe way to use remove():

bag = ["coin", "key"]

if "bomb" in bag:
    bag.remove("bomb")

print(bag)
['coin', 'key']

Nothing was removed and nothing crashed. Without the if, that remove() would have stopped the program.

Looping over a list

This is where lists and loops meet, and it is the reason both lessons exist:

enemies = ["ghosty", "bobo", "gigagantrum"]

for enemy in enemies:
    print("Spawning", enemy)
Spawning ghosty
Spawning bobo
Spawning gigagantrum

Read it out loud: for each enemy in enemies. The loop variable holds one item at a time, and the loop runs exactly as many times as there are items — you never write the number 3 anywhere. Add a fourth enemy to the list and the loop handles it with no other change. That is the whole point.

You do not usually need range() and index numbers to walk a list. But when you want the position as well as the item, enumerate() gives you both:

scores = [120, 340, 90]

for i, score in enumerate(scores):
    print(i, score)
0 120
1 340
2 90

A few more things lists do

scores = [120, 340, 90, 500]

print(sum(scores))
print(max(scores))
print(min(scores))
print(sorted(scores))
print(scores)
1050
500
90
[90, 120, 340, 500]
[120, 340, 90, 500]

Look at the last two lines. sorted() hands back a new sorted list and leaves the original alone — which is why scores still prints in its old order underneath. If you want to sort the list itself, scores.sort() does that and returns nothing.

And you can take a slice — a piece of a list — with two numbers:

scores = [500, 340, 120, 90, 10]

print(scores[0:3])
print(scores[:3])
print(scores[2:])
[500, 340, 120]
[500, 340, 120]
[120, 90, 10]

The same "up to but not including" rule as range(): scores[0:3] gives you items 0, 1 and 2, and stops before 3. Leaving a number out means "from the start" or "to the end". scores[:3] is how you take a top three.

Now put it on the screen

One list, one loop, a row of enemies:

from kaypy import *

kaypy(width=800, height=600, background=[20, 24, 36])
loadSprite("ghosty", "images/ghosty.png")
loadSprite("bobo", "images/bobo.png")
loadSprite("gigagantrum", "images/gigagantrum.png")

wave = ["ghosty", "bobo", "ghosty", "gigagantrum", "bobo"]

for i, name in enumerate(wave):
    add([sprite(name), pos(100 + i * 130, 260), area(), "enemy"])

Five enemies, spaced out, and the list decides which is which. i does the spacing exactly as it did in the loops lesson; name picks the sprite.

Change the list. Add "ghosty" to the end and run it — six enemies, no other edit. Put the same name in five times for a wave of one kind. Delete two and the row gets shorter. You are editing the data, not the code, and that is the difference a list makes.

Building a list as the game runs

Lists are most useful when they grow. Here every object that gets made is kept so you can do something to all of them later:

from kaypy import *

kaypy(width=800, height=600, background=[20, 24, 36])

coins = []

for i in range(6):
    coin = add([rect(20, 20), pos(80 + i * 100, 300), color(YELLOW)])
    coins.append(coin)

print("made", len(coins), "coins")

for coin in coins:
    coin.pos.y = coin.pos.y - 40
made 6 coins

The first loop makes them and remembers each one. The second loop moves every one of them 40 pixels up. Without the list you would have six variables and no way to talk about all of them at once.

Watch for

Counting starts at 0. The last item is at len(thing) - 1, or just -1.

IndexError: list index out of range means you asked for a position that is not there. Usually it is an off-by-one — check whether you meant - 1.

append() takes one item. bag.append("coin", "key") is an error. Two items means two calls, or bag.extend(["coin", "key"]).

remove() needs the item to be there, so check with in first.

sorted() gives a new list; .sort() changes the one you have and returns None. Writing scores = scores.sort() throws your list away and leaves you with None — and nothing complains until you try to use it.

An empty list is [], and if bag: is False when it is empty. You do not need if len(bag) > 0:.

Classwork 1 — The Inventory

A bag you can put things in and take things out of.

from kaypy import *

kaypy(width=800, height=600, background=[20, 24, 36])
loadSprite("coin", "images/coin.png")
loadSprite("key", "images/key.png")
loadSprite("lamp", "images/lamp.png")

bag = ["coin", "key"]

add([text("Bag:", size=28), pos(40, 40)])

One step at a time, running after each:

  1. Loop over bag and draw each item as a sprite in a row starting at pos(40, 100), 90 pixels apart. Use enumerate() for the spacing.
  2. Under the row, a label showing how many things you are carrying — use len(bag), not a number you typed.
  3. bag.append("lamp") before the drawing loop. Run it. Three sprites and the count says 3, and you did not touch the drawing code.
  4. A CAPACITY = 3 limit: only append when len(bag) < CAPACITY, otherwise print Bag is full to the console.
  5. Remove the key — safely. Check if "key" in bag: first, then bag.remove("key").

Stretch: draw the bag sorted alphabetically without changing the order you actually carry things in. Which of sorted(bag) and bag.sort() do you want, and why does it matter here?

Classwork 2 — The Wave Spawner

The list decides the wave. Each wave is harder than the last because the list gets longer.

from kaypy import *

kaypy(width=800, height=600, background=[20, 24, 36])
loadSprite("ghosty", "images/ghosty.png")
loadSprite("bobo", "images/bobo.png")
loadSprite("gigagantrum", "images/gigagantrum.png")

wave = ["ghosty", "ghosty"]
wave_number = 1
  1. Draw the wave: loop over it with enumerate() and put each sprite on screen, spaced across the window.
  2. A label at the top showing the wave number and how many enemies are in it.
  3. Make wave 2: wave.append("bobo") and add 1 to wave_number, then run it again. Three enemies.
  4. Make wave 3 harder still by appending "gigagantrum". Four enemies, and each wave is one line of change.
  5. Use a conditional from Lesson 3: if the wave has more than three enemies, label it DANGER — otherwise Clear.

Stretch: space the enemies using width() divided by len(wave) instead of a fixed 130 pixels, so the row always fits no matter how long the list gets. Add six more enemies and check that nothing runs off the edge.

Check yourself

What does this print?

loot = ["coin", "key", "coin"]

loot.remove("coin")
loot.append("gem")

print(loot)
print(len(loot))
print("coin" in loot)
['key', 'coin', 'gem']
3
True
Why is there still a coin?

remove() takes out the first match and then stops. There were two coins, so one of them is still there — at position 1, because everything after the removed item shifts down to fill the gap.

That is why "coin" in loot is still True, and why remove() in a loop catches people out: it does not remove all of something, only the next one it finds.



← Lesson 4 — Loops · All nine lessons · Lesson 6 — Functions →