Session 7 of the Python Fundamentals series, covered in depth. It introduces lists, which hold many values in order, covering how to create them, zero-based indexing, len(), negative indexing, and testing membership with in. It then covers the for loop, which makes one pass per item, range() in all three of its forms, looping by index, building a list with append(), and computing totals, averages, maxima, minima, and counts over a list. The traps are index out of range, the zero-based off-by-one, and range() stopping before its end value. Every snippet was copied verbatim from CPython 3.12.
Subject: Python Fundamentals · 96 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Python Fundamentals - Session 7
Store many things, and run through them one by one
Objectives
A while loop repeats until a condition; a for loop repeats once per item in a collection. By the end you can:
for, and count with range().append() and check membership with in.Warm-up
Discussion prompt
Before we open Session 7 - for Loops & Lists: without looking back, what was the main idea of Session 6 - while Loops, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
That session covers while loops, counters and accumulators, the infinite-loop trap, sentinels and flags, break and continue, and a guessing game.
Section
Part 1
Concept
A list stores many values in one variable, in order. Write it with square brackets, items separated by commas.
list — An ordered collection of values in square brackets: ["Ana", "Ben", "Cy"]. The order is kept, and you can look items up by position.
Counterexample
Discussion prompt
A list stores many values in one variable, in order. Write it with square brackets, items separated by commas.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Explain it to yourself
Discussion prompt
In Make a list this move is made:
Read the output
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
Verified by execution: prints the list with its brackets and quotes.
Worked example
team = ["Ana", "Ben", "Cy"]
print(team)One name, three values
Why: team holds all three strings, in the order written.
Read the output
Why: Verified by execution: prints the list with its brackets and quotes.
| expression | value |
|---|---|
| team | ['Ana', 'Ben', 'Cy'] |
Blank canvas
Draw it
Draw what Make a list just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
Get one item by its position in square brackets: team[0] is the first, team[1] the second.
Counting starts at 0, not 1. So the third item is team[2].
Analogy
Discussion prompt
Explain Indexing starts at 0 by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Get one item by its position in square brackets: team[0] is the first, team[1] the second.
Pattern
Predict first
The table runs: 0 | Ana · 1 | Ben
In Reading by position, given the rows so far: what is the next one — the row where index is 2?
Correct: 2 | Cy
| index | item |
|---|---|
| 0 | Ana |
| 1 | Ben |
| 2 | Cy |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. Verified by execution: prints Ana then Cy.
Worked example
team = ["Ana", "Ben", "Cy"]
print(team[0])
print(team[2])team[0] is the first item
Why: Position 0 is Ana.
team[2] is the third
Why: Verified by execution: prints Ana then Cy.
| index | item |
|---|---|
| 0 | Ana |
| 1 | Ben |
| 2 | Cy |
Comparison
Comparison matrix
From Reading by position: refill the item column from what you know. The rest of the table is as it appeared.
| index | item |
|---|---|
| 0 | Ana |
| 1 | Ben |
| 2 | Cy |
Intuition
Read the index as 'how many steps from the front'. The first item is 0 steps in, the second is 1 step in.
That is why a list of 3 items has indexes 0, 1, 2 - and no index 3.
Explain it
Discussion prompt
Explain Index = steps from the start to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Read the index as 'how many steps from the front'. The first item is 0 steps in, the second is 1 step in.
Concept
len(team) is how many items the list holds. For a 3-item list it is 3.
The last valid index is always len - 1, because indexing starts at 0.
Fill the middle
Fill in the blanks
From How many items? — one line has had its right-hand side removed. Put it back.
team = ["Ana", "Ben", "Cy"]
print(len(team))
print(team[len(team) - 1])
Why: team is what everything below it consumes, so the wrong expression here fails later and somewhere else. Verified by execution: team[2] is Cy.
Worked example
team = ["Ana", "Ben", "Cy"]
print(len(team))
print(team[len(team) - 1])len(team) is 3
Why: Three items in the list.
The last item is at index len - 1
Why: Verified by execution: team[2] is Cy. Prints 3 then Cy.
| expression | value |
|---|---|
| len(team) | 3 |
| team[len - 1] | Cy |
Trade off
Comparison matrix
From How many items?: every row here is a choice with a cost. Fill the value column, then say which row you would actually pick and what you give up for it.
| expression | value |
|---|---|
| len(team) | 3 |
| team[len - 1] | Cy |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Reaching for index 3 in a 3-item list.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Valid indexes are 0, 1, 2. Index 3 is one past the end, so Python raises an IndexError.
The last item is at index len - 1 (here, 2).
Why: Valid indexes are 0, 1, 2. Index 3 is one past the end, so Python raises an IndexError.
Trap
Reaching for index 3 in a 3-item list.
team = ["Ana", "Ben", "Cy"]
print(team[3])There is no index 3
Why: Valid indexes are 0, 1, 2. Index 3 is one past the end, so Python raises an IndexError.
| you write | result |
|---|---|
| team[3] | IndexError: list index out of range |
The last item is at index len - 1 (here, 2).
team = ["Ana", "Ben", "Cy"]
print(team[2])Stay within 0 .. len - 1
Why: team[2] is the last item, Cy. Real output: Cy. Or use team[-1] for 'the last', whatever the length.
| you write | value |
|---|---|
| team[2] | Cy |
Concept
team[-1] is the last item, team[-2] the second to last. Handy when you do not know the length.
Worked example
team = ["Ana", "Ben", "Cy"]
print(team[-1])
print(team[-2])-1 is the last, -2 the one before
Why: Negative indexes walk backward from the end.
Read the output
Why: Verified by execution: prints Cy then Ben.
| index | item |
|---|---|
| -1 | Cy |
| -2 | Ben |
Error analysis
Annotate
Walk the callouts on The last item, easily. Each one is a place this is easy to get subtly wrong.
Concept
"Ben" in team is True if that value is somewhere in the list, False otherwise.
A clean way to ask 'is this in my collection?' without a loop.
Worked example
team = ["Ana", "Ben", "Cy"]
print("Ben" in team)
print("Zed" in team)in scans for the value
Why: Ben is present; Zed is not.
Read the output
Why: Verified by execution: True then False.
| expression | value |
|---|---|
| "Ben" in team | True |
| "Zed" in team | False |
Section
Part 2
Concept
for item in team: runs its block once for each item, with item set to that value each pass.
No counter, no condition to update - the loop handles walking through the list for you.
Intuition
Read for name in team: as 'for each name in the team, do the following'.
The loop variable (name) is just a fresh label pointing at the current item; next pass it points at the next one.
Worked example
team = ["Ana", "Ben", "Cy"]
for name in team:
print("Go,", name)name takes each value in turn
Why: First Ana, then Ben, then Cy.
The body runs once per item
Why: Verified by execution: three lines, one per name.
| pass | name | prints |
|---|---|---|
| 1 | Ana | Go, Ana |
| 2 | Ben | Go, Ben |
| 3 | Cy | Go, Cy |
Pattern
Step through it
Step through Loop over the team one row at a time. What is driving the change, and what would the row after the last one be?
Concept
Use for when you know the collection to walk (a list, a range). Use while when you loop until a condition, with no fixed count.
A for loop cannot forget to update - it always advances - so it never accidentally loops forever.
Worked example
team = ["Ana", "Ben", "Cy"]
i = 0
while i < len(team):
print(team[i])
i += 1The while version needs a manual index
Why: You initialize i, check i < len, and remember i += 1 yourself.
for hides all that
Why: Verified by execution: prints Ana, Ben, Cy - same result as for name in team, but for is shorter and safer.
| i | team[i] |
|---|---|
| 0 | Ana |
| 1 | Ben |
| 2 | Cy |
Pattern
Step through it
Step through The same walk, two ways one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 3
Concept
for i in range(4): gives i the values 0, 1, 2, 3 - it stops before 4.
So range(n) produces exactly n numbers, starting at 0.
Worked example
for i in range(4):
print(i, end=" ")Counts 0, 1, 2, 3
Why: Four numbers, starting at 0, stopping before 4.
Read the output
Why: Verified by execution: 0 1 2 3.
| range(4) | gives |
|---|---|
| values | 0, 1, 2, 3 |
| count | 4 |
Concept
With two numbers, counting begins at start and stops before stop: range(1, 5) is 1, 2, 3, 4.
Sorting
Sort into buckets
These are the pieces of Session 7 - for Loops & Lists, out of order. Put each one back under the part of the lesson it belongs to.
Worked example
for i in range(1, 5):
print(i, end=" ")Starts at 1, stops before 5
Why: So 5 is not included.
Read the output
Why: Verified by execution: 1 2 3 4.
| range(1, 5) | gives |
|---|---|
| values | 1, 2, 3, 4 |
Concept
A third number is the step: range(2, 11, 2) counts 2, 4, 6, 8, 10.
The stop is still exclusive, so 11 does not appear even though the step would reach it.
Worked example
for i in range(2, 11, 2):
print(i, end=" ")Step of 2 skips the odds
Why: Starts at 2 and jumps by 2 each time.
Read the output
Why: Verified by execution: 2 4 6 8 10 (11 is excluded).
| range(2, 11, 2) | gives |
|---|---|
| values | 2, 4, 6, 8, 10 |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Expecting range(1, 5) to include 5.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: range(1, 5) is 1, 2, 3, 4 - the 5 never appears.
To include 5, stop at 6.
Why: range(1, 5) is 1, 2, 3, 4 - the 5 never appears. This off-by-one surprises everyone at first.
Trap
Expecting range(1, 5) to include 5.
for i in range(1, 5):
print(i, end=" ") # want 1..5?The stop value is excluded
Why: range(1, 5) is 1, 2, 3, 4 - the 5 never appears. This off-by-one surprises everyone at first.
| you write | actually gives |
|---|---|
| range(1, 5) | 1 2 3 4 |
To include 5, stop at 6.
for i in range(1, 6):
print(i, end=" ")Add 1 to the stop to include the last number
Why: range(1, 6) gives 1, 2, 3, 4, 5. Real output: 1 2 3 4 5.
| you write | gives |
|---|---|
| range(1, 6) | 1 2 3 4 5 |
Concept
for i in range(len(team)): gives each valid index, so you can use both the position i and the item team[i].
Reach for this when you need the index number, not just the value.
Fill the middle
Fill in the blanks
From Numbered list — one line has had its right-hand side removed. Put it back.
team = ["Ana", "Ben", "Cy"]
for i in range(len(team)):
print(i, team[i])
Why: team is what everything below it consumes, so the wrong expression here fails later and somewhere else. Verified by execution: 0 Ana, 1 Ben, 2 Cy.
Worked example
team = ["Ana", "Ben", "Cy"]
for i in range(len(team)):
print(i, team[i])i walks the valid indexes
Why: range(len(team)) is range(3) = 0, 1, 2.
Use i to reach both position and item
Why: Verified by execution: 0 Ana, 1 Ben, 2 Cy.
| i | team[i] |
|---|---|
| 0 | Ana |
| 1 | Ben |
| 2 | Cy |
Comparison
Comparison matrix
From Numbered list: refill the team[i] column from what you know. The rest of the table is as it appeared.
| i | team[i] |
|---|---|
| 0 | Ana |
| 1 | Ben |
| 2 | Cy |
Section
Part 4
Concept
team.append("Di") adds a new item to the end of the list, growing its length by one.
method — A function attached to a value with a dot, like team.append(x). It acts on that specific list.
Definition probe
Sort into buckets
Every line below is part of the definition of list or of method — one or the other, never both. Put each where it belongs.
Worked example
team = ["Ana", "Ben", "Cy"]
team.append("Di")
print(team, len(team))The new item lands at the end
Why: Di becomes index 3; the list now has four items.
Read the output
Why: Verified by execution: ['Ana', 'Ben', 'Cy', 'Di'] 4.
| before | after append |
|---|---|
| 3 items | 4 items (Di at index 3) |
Error analysis
Annotate
Walk the callouts on Growing the team. Each one is a place this is easy to get subtly wrong.
Concept
Start with an empty list [], then append inside a loop to build it up - the list version of an accumulator.
Worked example
squares = []
for i in range(1, 6):
squares.append(i * i)
print(squares)Start empty, append each square
Why: 1, 4, 9, 16, 25 get added one per pass.
Trace the list growing
Why: Verified by execution: [1, 4, 9, 16, 25].
| i | i*i | squares |
|---|---|---|
| 1 | 1 | [1] |
| 2 | 4 | [1, 4] |
| 3 | 9 | [1, 4, 9] |
| 4 | 16 | [1, 4, 9, 16] |
| 5 | 25 | [1, 4, 9, 16, 25] |
Intuition
The [] before the loop is an empty box. Each pass drops one more item in with append.
Put the squares = [] outside the loop - inside, it would reset to empty every pass and you would end with just one item.
Section
Part 5
Concept
Loop with an accumulator to add up a list, then divide by len() for the average.
Worked example
scores = [55, 84, 110, 65]
total = 0
for s in scores:
total += s
print(total, total / len(scores))Add each score to total
Why: total climbs 55, 139, 249, 314.
Average is total over count
Why: Verified by execution: 314 and 314 / 4 = 78.5.
| s | total |
|---|---|
| 55 | 55 |
| 84 | 139 |
| 110 | 249 |
| 65 | 314 |
Scale up
Step through it
Step through Average score and watch the numbers move. Now imagine the input ten times bigger: which column is the one that stops this being practical?
Concept
Python already knows how to total and find extremes: sum(scores), max(scores), min(scores).
For a simple total you rarely need to write the loop yourself - but understanding the loop is why these make sense.
Fill the middle
Fill in the blanks
From One-liners — one line has had its right-hand side removed. Put it back.
scores = [55, 84, 110, 65]
print(sum(scores))
print(max(scores))
print(min(scores))
Why: scores is what everything below it consumes, so the wrong expression here fails later and somewhere else. sum totals; max and min scan for the extremes.
Worked example
scores = [55, 84, 110, 65]
print(sum(scores))
print(max(scores))
print(min(scores))Each built-in walks the list for you
Why: sum totals; max and min scan for the extremes.
Read the output
Why: Verified by execution.
| call | value |
|---|---|
| sum(scores) | 314 |
| max(scores) | 110 |
| min(scores) | 55 |
Pattern
Step through it
Step through One-liners one row at a time. What is driving the change, and what would the row after the last one be?
Concept
To count how many items meet a rule, loop with a counter and an if inside: count += 1 when the item qualifies.
Pattern
Predict first
The table runs: 55 | no | 0 · 84 | yes | 1 · 110 | yes | 2
In How many passed?, given the rows so far: what is the next one — the row where s is 65?
Correct: 65 | yes | 3
| s | >= 60? | passing |
|---|---|---|
| 55 | no | 0 |
| 84 | yes | 1 |
| 110 | yes | 2 |
| 65 | yes | 3 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. Verified by execution: 84, 110, 65 pass; 55 does not.
Worked example
scores = [55, 84, 110, 65]
passing = 0
for s in scores:
if s >= 60:
passing += 1
print(passing)The if guards the counter
Why: Only scores of 60 or more bump passing.
Trace the count
Why: Verified by execution: 84, 110, 65 pass; 55 does not. Prints 3.
| s | >= 60? | passing |
|---|---|---|
| 55 | no | 0 |
| 84 | yes | 1 |
| 110 | yes | 2 |
| 65 | yes | 3 |
Pattern
Step through it
Step through How many passed? one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 6
Anomaly
Predict first
A student writes this, and it looks reasonable:
Thinking the second item is team[2].
It is wrong. Say what breaks — and say it before you turn the page.
Correct: team[2] is the THIRD item (Cy), not the second.
The second item is index 1.
Why: team[2] is the THIRD item (Cy), not the second. Human 'second' is index 1.
Trap
Thinking the second item is team[2].
team = ["Ana", "Ben", "Cy"]
print(team[2]) # want the 2nd, BenPosition counting starts at 0
Why: team[2] is the THIRD item (Cy), not the second. Human 'second' is index 1.
| you want | you wrote | you got |
|---|---|---|
| 2nd (Ben) | team[2] | Cy |
The second item is index 1.
team = ["Ana", "Ben", "Cy"]
print(team[1])Subtract 1 from the human position
Why: The Nth item is at index N - 1. team[1] is Ben. Real output: Ben.
| human | index |
|---|---|
| 1st | 0 |
| 2nd | 1 |
| 3rd | 2 |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
team[0] is the first, team[1] the second.; Read the index as 'how many steps from the front'. The first item is 0 steps in, the second is 1 step in.Concept
[] is a valid list with zero items. Looping over it simply does nothing - the body never runs.
This is why building with append from [] works: you start with a real (empty) list and grow it.
Explain it
Discussion prompt
Explain An empty list is fine to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
[] is a valid list with zero items. Looping over it simply does nothing - the body never runs.
Worked example
for x in []:
print("never runs")
print("done")No items means no passes
Why: The for body is skipped entirely.
The line after still runs
Why: Verified by execution: prints only done.
| list | body runs? | prints |
|---|---|---|
| [] | no | done |
Blank canvas
Draw it
Draw what Looping over nothing just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
In for s in scores:, s is a fresh label to the current item. Reassigning s inside the loop does not change the list.
To change the list itself, index into it (scores[i] = ...) using a range(len()) loop.
Analogy
Discussion prompt
Explain The loop variable is a copy by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
In for s in scores:, s is a fresh label to the current item. Reassigning s inside the loop does not change the list.
Concept
A list can hold strings, numbers, bools - whatever you need: [55, 84, 110] or ["Ana", "Ben"].
Usually you keep one kind per list (all scores, all names) so the same operation makes sense for every item you loop over.
Counterexample
Discussion prompt
A list can hold strings, numbers, bools - whatever you need: [55, 84, 110] or ["Ana", "Ben"].
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Usually you keep one kind per list (all scores, all names) so the same operation makes sense for every item you loop over.
Section
Part 7
Explain it to yourself
Discussion prompt
In Looping over a list this move is made:
Need the position? for i in range(len(things))
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
Gives the index i, then use things[i] for the value.
Pattern
Just the items? for item in things
Why: Cleanest when you only need each value.
Need the position? for i in range(len(things))
Why: Gives the index i, then use things[i] for the value.
Fixed count of numbers? for i in range(a, b)
Why: Counts a up to b-1. Remember the stop is exclusive.
Pattern
Total or average: accumulate then / len
Why: total = 0 before, total += item inside, total / len(list) after. Or just sum(list).
Count matches: counter + if
Why: count = 0, then count += 1 when the item meets your rule.
Build a new list: [] + append
Why: result = [] before, result.append(...) inside.
Real world
Discussion prompt
Outside this lesson: where does Session 7 - for Loops & Lists actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Computing over a list is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
Session 7 of the Python Fundamentals series, in depth. Lists that hold many values in order: creating them, 0-based indexing, len(), negative indexing, and membership with in.
Check
Which item is this?
colors = ["red", "green", "blue"]
print(colors[1])| index | item |
|---|---|
| 0 | red |
| 1 | ? |
Check your understanding
What does this print?
Answer: A
Why: Indexing starts at 0, so index 1 is the second item, green. Verified by execution.
Check
How many numbers?
for i in range(3):
print(i)| range(3) | values |
|---|---|
| ? |
Check your understanding
What does this print?
Answer: A
Why: range(3) starts at 0 and stops before 3, giving 0, 1, 2 - three numbers. Verified by execution.
Check
Where does it stop?
for i in range(2, 6):
print(i, end=" ")| range(2, 6) | values |
|---|---|
| ? |
Check your understanding
What does this print?
Answer: A
Why: range(2, 6) starts at 2 and stops before 6, so it is 2, 3, 4, 5. Verified by execution.
Check
Trace total.
nums = [10, 20, 30]
total = 0
for n in nums:
total += n
print(total)| n | total |
|---|---|
| 10 | ? |
| 30 | ? |
Check your understanding
What does this print?
Answer: A
Why: It adds 10 + 20 + 30 = 60. total climbs 10, 30, 60. Verified by execution.
Trade off
Comparison matrix
From Check: the total: every row here is a choice with a cost. Fill the total column, then say which row you would actually pick and what you give up for it.
| n | total |
|---|---|
| 10 | ? |
| 30 | ? |
Check
What is in doubled at the end?
doubled = []
for n in [1, 2, 3]:
doubled.append(n * 2)
print(doubled)| n | appends |
|---|---|
| 1 | 2 |
| 3 | 6 |
Check your understanding
What does this print?
Answer: A
Why: Each item is doubled and appended: 2, 4, 6. The list grows one item per pass. Verified by execution.
Comparison
Comparison matrix
From Check: append in a loop: refill the appends column from what you know. The rest of the table is as it appeared.
| n | appends |
|---|---|
| 1 | 2 |
| 3 | 6 |
Check
Is it there?
fruits = ["apple", "pear"]
print("grape" in fruits)| expression | value |
|---|---|
| "grape" in fruits | ? |
Check your understanding
What does this print?
Answer: A
Why: grape is not one of the items, so "grape" in fruits is False. Verified by execution.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Lists: Many Values, One Name · The for Loop · range(): counting · Changing a List · Computing over a List · Common Pitfalls. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
A list holds many values in order; index from 0; len() gives the size. A for loop runs once per item, and range() counts for you.
| You want | Use |
|---|---|
| each item | for item in things |
| the positions | for i in range(len(things)) |
| a count of numbers | for i in range(a, b) |
| add an item | things.append(x) |
| a total | sum(things) |
| is it present? | x in things |
Remember: indexes start at 0, and range/index go up to len - 1. Next session we meet dictionaries - look values up by name instead of by position.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.