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]
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
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.
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.
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.
Key Takeaways
-
for x in collectionvisits every item. Python assigns each item to the loop variable, runs the indented body, and moves to the next item. When the collection is empty, the body never runs. -
inhas two meanings. In aforstatement,inmeans "take each item from." In an expression like"ai" in tags, it means "is this item a member of." Context determines which meaning applies. -
range()generates numeric sequences.range(5)produces 0 through 4.range(2, 7)produces 2 through 6. The stop value is always excluded. -
The accumulator pattern builds results incrementally. Start with an initial value (0 for sums,
""for strings,[]for lists), then update it with+=or.append()on each iteration. -
Always test the empty collection case. When the list is empty, the loop body never runs and the accumulator keeps its initial value. This is correct behavior, but easy to miss if you only test with populated lists.
Looking Ahead
You can now iterate collections and build results one item at a time. But what if you also need the position of each item? In Lesson 3, you will learn enumerate(), which gives you both the index and the value on each iteration. You will also see how loops interact with the collection methods from Chapter 48.