Visual walkthrough — for loop — iterating over sequences
We already met this idea in the parent note, the for-loop topic note. Here we slow all the way down and watch it happen frame by frame.
Step 1 — Start with the thing you want to walk through
WHAT. We have a box of items laid out in a row. In Python this is called a sequence —
a list, a string, a range, etc. Let us use a tiny list of three fruits:
seq = ["apple", "banana", "cherry"]WHY start here. Before any looping can happen, there must be something ordered to loop over. "Ordered" means the items have positions: a 0th, a 1st, a 2nd. (Python counts from zero — see Lists and indexing for why.) The loop's whole job is to visit these positions left to right, exactly once each.
PICTURE. Three boxes in a row, each with a position number written under it.

Step 2 — Build the cursor (iter)
WHAT. Before the loop runs its first pass, Python calls iter(seq) once. This hands back a
brand-new object called an iterator. Think of it as a little arrow — a cursor — parked
just before the first item.
_it = iter(seq) # a cursor sitting before index 0WHY this and not just an index number? We could keep a plain counter i = 0 and read
seq[i]. But that only works for things with numbered slots. Many iterables (files, network
streams, generators — see Iterators and generators) have no index at all; you can only ask
them "what's next?" The iterator is the universal answer: it hides how position is tracked, and
just promises to remember it.
PICTURE. The same three boxes, now with a red cursor arrow sitting before box 0.

Each symbol in _it = iter(seq):
seq— the lineup from Step 1 (the thing being walked).iter(...)— the factory that produces a fresh cursor for it._it— our name for that cursor (the underscore just marks it as internal machinery).
Step 3 — The next move: fetch one item and advance
WHAT. To get an item we press the next button: next(_it). Two things happen together:
- it returns the item the cursor is about to pass, and
- it advances the cursor one step forward.
x = next(_it) # x == "apple"; cursor now sits before index 1WHY these two things at once? This is the heart of why loops never repeat or skip on their own. Because reading and advancing are welded together, you cannot read the same item twice by accident, and you cannot forget to move on. One press = one item = one step forward. Always.
PICTURE. The cursor has jumped from "before box 0" to "before box 1"; the value "apple"
flies out into the variable x.

Term by term in x = next(_it):
_it— our cursor (it holds the current position).next(...)— press the button: hand back current item, then step forward.x— the loop variable: a box that catches the item so the body can use it.
Step 4 — Run the body, then loop back
WHAT. Now that x holds one real item, we run the loop body — the indented code you wrote.
Then we go back to Step 3 and press next again for the following item.
print(x.upper()) # APPLE ← the body, run with this x
# ...then back to next(_it) for "banana"WHY "body then back"? This is the pattern that makes you write the body once but run it
as many times as there are items. The body is a stamp; each next inks it with a fresh item.
PICTURE. A cycle: next → body → next → body → …. Watch the cursor march box by box while the
body fires once per stop.

Step 5 — The end signal: StopIteration
WHAT. After "cherry", the cursor sits past the last box. The next press next(_it) has
nothing left to hand back — so instead of returning a value, it raises a special signal called
==StopIteration==.
x = next(_it) # cursor is past the end -> raises StopIterationWHY signal with an exception instead of counting? If the loop had to count items ("stop after 3"), it would need to know the length up front — impossible for streams that don't know their own length. Instead the iterator itself shouts "I'm empty!" exactly when it runs dry. No counting, so no famous off-by-one bugs.
PICTURE. The cursor is now beyond the last box; pressing next triggers a red StopIteration
flag instead of a value.

Step 6 — Assemble the machine: for is a while
WHAT. Put Steps 2–5 together and you have written for yourself. These two are exactly the
same program:
for x in seq: # ⇕ identical behaviour
body(x)_it = iter(seq) # Step 2: build the cursor (ONCE)
while True: # loop forever...
try:
x = next(_it) # Step 3: fetch + advance
except StopIteration:
break # Step 5: empty -> stop cleanly
body(x) # Step 4: run the bodyWHY this desugaring matters. Every claim in the parent note now has a reason:
the loop variable moves only forward (cursor made once, in Step 2); the loop ends cleanly with
no counting (Step 5's signal); and for is genuinely built on the while loop
plus the iterator protocol.
PICTURE. The while-machine flowchart, with each box tagged by the step that built it.

Step 7 — The dangerous edge case: editing the box mid-walk
WHAT. The cursor secretly tracks a position number, not the identity of an item. If you remove an item while walking, everything to its right shifts left — but the cursor still advances to the next position, so it skips the item that slid into the gap.
nums = [1, 2, 3, 4]
for n in nums:
if n % 2 == 0:
nums.remove(n) # result: [1, 3] — the 4 survives!WHY it goes wrong (traced):
- cursor at pos 0 →
1(odd, keep). Advance to pos 1. - cursor at pos 1 →
2(even, remove). Now list is[1, 3, 4];3slid into pos 1. Cursor advances to pos 2. - cursor at pos 2 →
4(the3was skipped!).4is even → remove. List[1, 3]. - cursor at pos 3 → past end →
StopIteration→ stop.
PICTURE. Before/after boxes showing 3 sliding under the cursor while the cursor steps over it.

Step 8 — The empty box: zero iterations, zero fuss
WHAT. What if the sequence is empty? Then iter([]) gives a cursor already past the (non-
existent) end, so the very first next raises StopIteration, and the body runs zero times.
for x in []:
print("this never prints")
print("done") # prints: doneWHY show this. Beginners fear "an empty loop will crash." It can't — the machine we built in
Step 6 handles it for free: the while True starts, the first next fails, break fires, we
leave. Empty is just "the general case with nothing in it."
PICTURE. An empty rail: cursor starts past the end, next immediately raises the flag, body
count = 0.

The one-picture summary
Everything above, compressed into one diagram: cursor is born (iter), then the
next → body cycle turns until the box runs dry and StopIteration breaks the loop.

Recall Feynman retelling — the whole walkthrough in plain words
Picture a lunchbox of snacks in a row. First, a kid puts a finger just before the first snack
— that finger is the cursor (iter, Step 2). Every turn the kid does one move: grab the snack
the finger points at and slide the finger to the next one — that's the next button, and
because grabbing and sliding happen together, no snack is ever grabbed twice or skipped
(Step 3). With a snack in hand, the kid does something with it — the body (Step 4). Then repeat.
When the finger slides past the last snack, the next grab finds nothing and the kid says
"empty!" — that's StopIteration, the clean stop (Step 5). Glue those moves together and you
literally get a while loop that keeps pressing next until it hears "empty" (Step 6). Two
warnings: if you take snacks out mid-walk, the row shifts under the finger and one snack gets
stepped over (Step 7); and if the lunchbox is empty to begin with, the kid grabs zero snacks
and just walks away — no crash (Step 8). That is the entire for loop, no magic left.
Connections
- while loop — condition-based repetition — the
formachine is this, per Step 6. - Iterators and generators — the
iter/next/StopIterationprotocol from Steps 2–5. - range function — a lazy iterable that produces the same cursor behaviour for numbers.
- Lists and indexing — why positions start at (Step 1) and why shifting bites in Step 7.
- Strings as sequences — a string is just another lineup of boxes (characters).
- List comprehensions — the safe rebuild fix from Step 7.
- enumerate and zip — cursors that also hand you the index / walk two boxes in lockstep.