What we're doing today
By the end of this session, you will know how to reach into a list and pull out one item, count from the back, grab a stretch of items in one go, add new things, take things out, change what is already there, and ask whether something is in the list at all. You will also get your first taste of a shorter way to build lists.
Last week you saw lists in passing. We made one, looped over it, moved on. Today we slow down and look at the list itself.
What a list actually is
Picture a row of numbered seats in a lecture hall. Seat 0 on the far left, then seat 1, then seat 2, all the way down. The seats are in a fixed order. Each one has a number that does not change. A list in Python is exactly that — a row of slots, in order, each slot holding one thing.
You make a list with square brackets and commas:
names = ["Ama", "Kojo", "Esi", "Yaw"]That line creates four slots, in order. Ama is in the first slot, Kojo in the second, and so on. The whole row of slots is stored in a labelled jar called names, exactly the way we stored single things last week.
Reaching in by position
To pull one item out of the list, you write the list's name, then square brackets, then the position you want.
names = ["Ama", "Kojo", "Esi", "Yaw"]
print(names[0]) # Ama
print(names[1]) # Kojo
print(names[3]) # YawThat number inside the brackets has a name. It is called the index — the number-tag that says which slot you want. names[0] means "give me the item from slot 0." That is all an index is. A position number.
Now for the part that throws everyone on day one: the first slot is numbered 0, not 1. That feels wrong — you have been counting from 1 your whole life. Let me be honest about where this comes from.
In the old days of programming, the number in the brackets was not "which item, counting from one." It was the offset from the start — how many steps you had to walk from the beginning of the list to reach the item. The first item is zero steps from the start. The second is one step. The third is two. Python inherited that convention, and almost every language uses the same one. Read names[0] in your head as "zero steps from the start," and the strangeness fades after a week.
If you ask for a slot that does not exist, Python does not invent one:
print(names[10]) # IndexError: list index out of rangeIndexError: list index out of range is Python being honest. There are four items in the list. The valid indexes are 0, 1, 2, and 3. There is no slot 10. When you see that error, check how many items are actually in the list with len(names):
print(len(names)) # 4Four items. The last valid index is len(names) - 1, which is 3. That subtraction trips people up — we'll come back to it in the slip-ups.
Counting from the back
You can also point at items from the end of the list, using negative numbers. These are called negative indices — same word as before, "index" is just the number-tag, but now the tag is allowed to be negative.
print(names[-1]) # Yaw
print(names[-2]) # Esi-1 means "the last item." -2 means "second from the last." Any time you want the most recent thing — the last message someone sent, the newest name on a guest list — names[-1] is the move. You do not need to know how long the list is. Python counts back for you.
Slicing — asking for a stretch
Sometimes you do not want one item. You want a range — the first three, the last two, the middle bit. Asking for a stretch of the list like that is called a slice, written with a colon inside the square brackets.
names = ["Ama", "Kojo", "Esi", "Yaw", "Kwame"]
first_three = names[0:3] # ["Ama", "Kojo", "Esi"]
last_two = names[-2:] # ["Yaw", "Kwame"]
middle = names[1:4] # ["Kojo", "Esi", "Yaw"]Read names[1:4] as "give me a stretch starting at slot 1, up to but not including slot 4." That last part matters: the start position is included, the end position is left out. [1:4] gives you slots 1, 2, and 3. You do not get slot 4.
Why exclude the end? Because of one quiet, useful property: the number of items you get back is always end - start. [1:4] gives you 4 - 1 = 3 items. The number you ask for is the number you get.
A slice always builds a brand new list. The original is untouched.
Adding things to a list
A list is not frozen. You can grow it, shrink it, change what is in it. The most common move is adding one item to the end:
names = ["Ama", "Kojo", "Esi"]
names.append("Yaw")
print(names) # ["Ama", "Kojo", "Esi", "Yaw"].append("Yaw") means "stick Yaw onto the end of this list." Almost every program where you collect things — names that passed a check, prices below a limit — starts with an empty list and appends as it goes:
fruits = ["apple", "banana", "cherry", "date", "fig"]
short_names = []
for fruit in fruits:
if len(fruit) <= 5:
short_names.append(fruit)
print(short_names) # ["apple", "date", "fig"]Make an empty list. Loop through the data. If an item matters, append it. You will write that shape a hundred times.
If you want to add an item somewhere other than the end, use .insert:
names = ["Ama", "Kojo", "Yaw"]
names.insert(1, "Esi")
print(names) # ["Ama", "Esi", "Kojo", "Yaw"]names.insert(1, "Esi") means "put Esi at slot 1, and push everyone else along." Think of a queue at a bus station — someone cuts in at position 1, and everybody behind them shuffles back one step.
Taking things out
There are two everyday ways to remove an item. The first is .remove, which takes the item itself:
names = ["Ama", "Kojo", "Esi", "Yaw"]
names.remove("Kojo")
print(names) # ["Ama", "Esi", "Yaw"]Two things to know. .remove only removes the first match — if Kojo appeared twice, only the first one would go. And if the item is not in the list at all, .remove crashes with an error. If you are not sure the item is there, check first with in, which we'll meet in a moment.
The second way is .pop, which takes a position, removes that item, and hands it back to you:
names = ["Ama", "Kojo", "Esi", "Yaw"]
last = names.pop() # removes "Yaw" and returns it
first = names.pop(0) # removes "Ama" and returns it
print(last, first) # Yaw Ama
print(names) # ["Kojo", "Esi"]With no number, .pop() takes the last item off. With a number, it takes the item at that slot. Use .pop when you care about what came out, not just that it left.
Changing one item
To swap one item for another, assign to its slot:
names = ["Ama", "Kojo", "Esi"]
names[1] = "Kweku"
print(names) # ["Ama", "Kweku", "Esi"]Same square brackets as reading, but now on the left of the =. Read it as "store Kweku in slot 1, replacing whatever was there."
Is it in the list?
To ask whether something is already in a list, use the word in. It is called the membership operator — a fancy name for a simple question: is this thing a member of that list, yes or no? The answer is True or False.
guest_list = ["Ama", "Kojo", "Esi"]
if "Ama" in guest_list:
print("Ama is invited.")
if "Yaw" not in guest_list:
print("Yaw is not on the list.")Think of the guest list at a wedding. The person at the door reads your name, looks at the paper, and either lets you through or turns you back. in does the same thing — looks through the list, returns True if your name is there, False if not.
This is the same in from for fruit in fruits. Same word, related idea. One uses the answer to walk through the list; the other uses it to make a yes-or-no decision.
A first taste of a shorter way
Once you've written the empty-list-then-append shape a few times, Python offers a shorthand called a list comprehension — the name sounds heavy, but it is just a one-line way to build a new list from an old one.
names = ["ama", "kojo", "esi", "yaw"]
shouted = [name.upper() for name in names]
print(shouted) # ["AMA", "KOJO", "ESI", "YAW"]Read that line in plain English: "a new list, made of name.upper(), for each name in names." It is the same as this longer version:
shouted = []
for name in names:
shouted.append(name.upper())Both produce the same list. The comprehension just compresses it. Do not force this. If the long version is clearer today, write the long version. Python does not pay you by the line.
Your exercise
Open a Colab cell and start with this list:
names = ["ama", "kojo", "esi", "yaw", "kwame", "akua", "nana"]Then do the following:
- Print the first name, then the last name, using indexing.
- Print a slice of the middle three names.
- Add a new name to the end with
.append. Add another at slot 0 with.insert. Print the list. - Remove one name with
.remove. Use.popto take out another, store what comes back in a variable, and print it. - Ask whether
"Ama"is in the list, and print a sentence saying so. - Build a new list called
capitalisedwhere every name starts with a capital letter. Try it first with the empty-list-then-append shape. Then rewrite it as a one-line list comprehension usingname.capitalize().
Take a screenshot of the final cell when it works.
Common slip-ups
- Off-by-one with indexing. A list of four items has indexes 0, 1, 2, 3 — not 1, 2, 3, 4. The last valid index is always
len(my_list) - 1. - IndexError on a short list. If Python says
IndexError: list index out of range, you asked for a slot that is not there. Printlen(my_list)and look at what is actually in it. - The "up to but not including" trap.
names[1:4]gives you slots 1, 2, and 3. Slot 4 is left out. If you wanted slot 4 included, writenames[1:5]. .removeon something that is not there. It crashes. If unsure, check withif item in my_listfirst.- The two kinds of square brackets.
["Ama", "Kojo"]with no name in front of it makes a new list.names[0]with a name in front of it reaches into an existing list. Same symbol, different job, decided by what is on the left.
That's the session. Run the exercise, screenshot it, and bring questions to the next live session. You can now do almost everything you'll ever need to do with a list. The rest is practice.