For Loops: Iteration Basics
James has a problem. He is staring at a list of tags from five different SmartNotes, and he needs to count how many tags mention "python." He starts writing five separate if statements, one for each note.
Emma watches him for about ten seconds before she intervenes. "What happens when you have a hundred notes?"
James stops typing. A hundred separate if blocks is not realistic. There has to be a better way.
"There is," Emma says. "You've already seen it. Remember for file in files back in Chapter 43?" She pulls up one of the axiom examples they read together. "You predicted what that loop did. Now you write your own."
The in Keyword: Two Meanings
Before writing a loop, one important clarification. You already know in from Chapter 48, where it checks membership:
tags: list[str] = ["python", "ai", "beginner"]
print("ai" in tags) # True
print("rust" in tags) # False
Output:
True
False
Inside a for statement, in means something different. Instead of asking "is this item in the collection?" it means "take each item from the collection, one at a time." Same keyword, two roles depending on context.
A loop repeats a block of code multiple times. Instead of writing the same instructions over and over, you write them once inside the loop body, and Python runs them for each item in your collection. If you have a list of 10 tags, the loop body runs 10 times. If you have 1,000 tags, it runs 1,000 times. You write the logic once.
Your First for Loop
The pattern is: for variable in collection: followed by an indented body.
tags: list[str] = ["python", "ai", "beginner"]
for tag in tags:
print(f"Tag: {tag}")
Output:
Tag: python
Tag: ai
Tag: beginner
Here is what happens step by step:
| Iteration | tag value | Action |
|---|---|---|
| 1 | "python" | Prints Tag: python |
| 2 | "ai" | Prints Tag: ai |
| 3 | "beginner" | Prints Tag: beginner |
Python takes the first item from tags, assigns it to tag, runs the indented body, then moves to the next item. When the list is exhausted, the loop ends and Python continues with whatever comes after.
Notice the indentation. The loop body follows the same 4-space rule as function bodies and if blocks from Lesson 1. Everything indented under for tag in tags: runs on each iteration. The first un-indented line after the body is outside the loop.
Emma points at the variable name. "What type does tag have inside the loop?"
James checks: tags is list[str], so each element is a str. That means tag is a str on every iteration. The type system follows you into loops.
range() for Numeric Iteration
Sometimes you need to repeat something a fixed number of times, or you need the numbers themselves. The range() function generates a sequence of integers.
for i in range(5):
print(i)
Output:
0
1
2
3
4
range(5) produces five numbers starting from 0 and stopping before 5. The stopping point is excluded, just like slicing in Chapter 48.
You can also provide a start value:
for i in range(2, 7):
print(i)
Output:
2
3
4
5
6
range(2, 7) starts at 2 and stops before 7. The pattern is range(start, stop), where start is included and stop is excluded.
| Call | Numbers produced |
|---|---|
range(5) | 0, 1, 2, 3, 4 |
range(2, 7) | 2, 3, 4, 5, 6 |
range(1, 4) | 1, 2, 3 |
range(0, 0) | (nothing) |
The += Shorthand
Before building your first accumulator, you need one small piece of syntax. You saw total += count in Chapter 43's program examples. Here is what it means:
total: int = 0
total = total + 10 # Explicit version
total += 5 # Shorthand: same thing
print(total)
Output:
15
total += 5 is shorthand for total = total + 5. It reads as "add 5 to whatever total already holds." Both forms do the same thing. The shorthand is shorter and easier to scan inside loops.
The Accumulator Pattern
The accumulator pattern is one of the most common loop patterns in programming. You start with an initial value (usually 0 for numbers or an empty string for text), then update it on every iteration.
scores: list[int] = [85, 92, 78, 95]
total: int = 0
for score in scores:
total += score
average: float = total / len(scores)
print(f"Total: {total}, Average: {average}")
Output:
Total: 350, Average: 87.5
Here is the trace, iteration by iteration:
| Iteration | score | total before | Operation | total after |
|---|---|---|---|---|
| 1 | 85 | 0 | 0 + 85 = 85 | 85 |
| 2 | 92 | 85 | 85 + 92 = 177 | 177 |
| 3 | 78 | 177 | 177 + 78 = 255 | 255 |
| 4 | 95 | 255 | 255 + 95 = 350 | 350 |
After the loop, total is 350 and len(scores) is 4, so the average is 87.5. The accumulator pattern works because each iteration builds on the previous result.
Combining Loops with Branches
You can put an if block (from Lesson 1) inside a loop body. This lets you filter items as you iterate.
word_counts: list[int] = [350, 1200, 80, 500]
long_count: int = 0
for count in word_counts:
if count > 1000:
long_count += 1
print(f"Notes over 1000 words: {long_count}")
Output:
Notes over 1000 words: 1
The loop visits every item. The if inside the loop body decides whether to count it. Only 1200 passes the condition, so long_count ends at 1. Notice the double indentation: the if is indented under for, and long_count += 1 is indented under if.
Here is a slightly richer example. James wants to sort tags into two groups:
tags: list[str] = ["python", "ai", "beginner", "data", "advanced"]
short_tags: list[str] = []
long_tags: list[str] = []
for tag in tags:
if len(tag) <= 4:
short_tags.append(tag)
else:
long_tags.append(tag)
print(f"Short: {short_tags}")
print(f"Long: {long_tags}")
Output:
Short: ['ai', 'data']
Long: ['python', 'beginner', 'advanced']
Each tag passes through the if/else gate inside the loop, landing in one list or the other.
Testing Loop-Based Functions
Loops make functions more powerful, but they also introduce more paths to test. Wrap your loop logic in a function and use assert to verify it:
def sum_scores(scores: list[int]) -> int:
total: int = 0
for score in scores:
total += score
return total
# Test: normal case
assert sum_scores([85, 92, 78, 95]) == 350
# Test: single item
assert sum_scores([100]) == 100
# Test: empty list
assert sum_scores([]) == 0
print("All tests passed.")
Output:
All tests passed.
The empty list test is important. When scores is empty, the loop body never runs, and total stays at 0. That is the correct behavior for summing nothing, but it is easy to overlook if you only test with full lists.
PRIMM-AI+ Practice: Tracing Loops
Predict [AI-FREE]
Press Shift+Tab to enter Plan Mode before predicting.
For each code snippet below, predict the final value of the variable after the loop. Write your prediction and a confidence score from 1 to 5 before checking the answers.
Snippet 1:
total: int = 0
for n in [10, 20, 30]:
total += n
# What is total?
Snippet 2:
count: int = 0
for x in range(4):
count += 1
# What is count?
Snippet 3:
result: str = ""
for word in ["hello", "world"]:
result += word + " "
# What is result?
Snippet 4:
big: int = 0
for val in [5, 15, 3, 22, 8]:
if val > 10:
big += 1
# What is big?
Snippet 5:
total: int = 0
for i in range(1, 6):
total += i
# What is total?
Check your predictions
Snippet 1: total is 60. The loop adds 10 + 20 + 30. Starting from 0: 0 + 10 = 10, 10 + 20 = 30, 30 + 30 = 60.
Snippet 2: count is 4. range(4) produces 0, 1, 2, 3 (four values). The loop adds 1 for each, so count goes 0 → 1 → 2 → 3 → 4.
Snippet 3: result is "hello world ". The accumulator starts as an empty string. Each iteration appends the word plus a space. Note the trailing space after "world."
Snippet 4: big is 2. Only 15 and 22 are greater than 10. The values 5, 3, and 8 fail the condition.
Snippet 5: total is 15. range(1, 6) produces 1, 2, 3, 4, 5. The sum is 1 + 2 + 3 + 4 + 5 = 15.
If you got 4-5 correct, your loop tracing is solid. If you missed Snippet 3, string accumulation is the tricky one because the trailing space is easy to overlook.
Run
Press Shift+Tab to exit Plan Mode.
Create a file called loop_practice.py with the snippets above (add print() calls to display the results). Run uv run python loop_practice.py and compare the output to your predictions.
Investigate
For any prediction you got wrong, build a trace table like the one in the Accumulator Pattern section. Write out each iteration: what the loop variable is, what the accumulator holds before and after. Trace tables make the logic visible.
If you want to go deeper, run /investigate @loop_practice.py in Claude Code and ask about the difference between range(4) and range(1, 5).
Modify
Change Snippet 4 so it counts values less than or equal to 10 instead. Predict the new result before running. Then change the threshold from 10 to 20 and predict again.
Make [Mastery Gate]
Write a function count_long_words(words: list[str], min_length: int) -> int that returns how many words have length greater than or equal to min_length. Test it with:
assert count_long_words(["hi", "hello", "hey"], 4) == 1
assert count_long_words(["python", "ai", "data"], 3) == 2
assert count_long_words([], 5) == 0
Run uv run python on your file. All assertions passing means you have mastered the basics.
Exercises
Exercise 1: Trace Table
Hand-trace this loop. Fill in the table below with the value of running_total at the end of each iteration.
prices: list[float] = [9.99, 24.50, 3.75, 15.00]
running_total: float = 0.0
for price in prices:
running_total += price
| Iteration | price | running_total (after) |
|---|---|---|
| 1 | 9.99 | ? |
| 2 | 24.50 | ? |
| 3 | 3.75 | ? |
| 4 | 15.00 | ? |
Check your trace
| Iteration | price | running_total (after) |
|---|---|---|
| 1 | 9.99 | 9.99 |
| 2 | 24.50 | 34.49 |
| 3 | 3.75 | 38.24 |
| 4 | 15.00 | 53.24 |
Exercise 2: Write the Test
A function sum_scores(scores: list[int]) -> int returns the total of all scores. Write three assert tests for it:
- Empty list: what should the sum of no scores be?
- Single item:
[42]should return what? - Many items:
[10, 20, 30, 40]should return what?
def sum_scores(scores: list[int]) -> int:
total: int = 0
for score in scores:
total += score
return total
# Write your tests below:
assert sum_scores([]) == ___
assert sum_scores([42]) == ___
assert sum_scores([10, 20, 30, 40]) == ___
Check your tests
assert sum_scores([]) == 0
assert sum_scores([42]) == 42
assert sum_scores([10, 20, 30, 40]) == 100
The empty list case returns 0 because the loop body never executes and total stays at its initial value. This is a common edge case that catches bugs: if someone initializes total to something other than 0, the empty list test will fail.
Try With AI
If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.
Prompt 1: Check Your Understanding
I just learned about Python for loops. Here is my understanding:
"A for loop visits every item in a list and runs the body once
for each item. The loop variable gets reassigned on each
iteration." Is my summary accurate? What am I missing?
Read the response carefully. Did it mention that the loop body does not run at all if the collection is empty? Did it clarify what "reassigned" means? Compare its correction to the empty list behavior you tested in the exercises.
What you're learning: You are testing your own mental model by stating it out loud and letting the AI find gaps. This is the same pattern as PRIMM's "Predict" phase, but applied to concepts instead of code.
Prompt 2: Generate and Review a Loop
Write a typed Python function called count_tags that takes a
list[str] of tags and a str target, and returns the number of
times target appears in the list. Use a for loop and an
accumulator. Include 3 assert tests.
Read the output. Check: does it use type annotations on every variable? Does it test the empty list case? Does the accumulator start at 0? If anything is missing, tell the AI what to fix.
What you're learning: You are reviewing AI-generated loop logic against the patterns from this lesson. The accumulator pattern, the empty list edge case, and proper type annotations are your checklist.
Prompt 3: Generate Tests for a Loop Function
Here is a function. Write 4 assert-based tests for it, including
an empty list case, a single-item case, and a boundary case.
def total_above_threshold(values: list[int], threshold: int) -> int:
total: int = 0
for v in values:
if v > threshold:
total += v
return total
Read the AI's tests before running them. Did it include the empty list case? Did it test what happens when no values exceed the threshold? If any case is missing, write it yourself.
What you're learning: You are practicing test design for loop-based functions, reinforcing the "empty, single, many" testing discipline from this lesson.
Every SmartNotes feature that works with lists (counting tags, summing word counts, filtering notes by length) uses the for loop and accumulator pattern from this lesson. When you build SmartNotes functions in later chapters, these patterns will be the foundation. The loop handles the repetition; the accumulator collects the result.
James leans back. "A for loop visits every item. range() generates the numbers. And the accumulator pattern -- start at zero, add on every pass -- that's how you build a total." He thinks for a second. "It's like a conveyor belt at the distribution center. Each package comes down the line, you scan it, update the running count. The belt does not care if there are five packages or five thousand. You wrote the scanning logic once."
"What happens when the belt is empty?" Emma asks.
"The loop body never runs. The accumulator stays at its initial value." He grins. "I almost forgot to test that. Empty list, single item, normal list -- three tests minimum."
"Good. I once shipped a sum function that crashed on empty input because I divided by len(scores) without checking for zero first." Emma shakes her head. "The empty-list test would have caught it in ten seconds."
"One thing bugged me, though. I wanted to print each tag with its position number, like 'Tag 0: python.' I set up a counter, incremented it manually. Worked, but felt clunky."
"That's exactly what Lesson 3 fixes. enumerate() gives you both the index and the value. You also get to mutate lists with .remove(), .pop(), and .sort()."