1.3.10 · D2Python Intermediate

Visual walkthrough — Iterators — `__iter__`, `__next__`, StopIteration

2,069 words9 min readBack to topic

Before we begin, three words we will earn through pictures — do not memorise them yet, just meet them:

  • iterable — a thing you can ask for a fresh player.
  • iterator — the player itself; it has one button.
  • StopIteration — the click the empty player makes.

Step 1 — A sequence is just "items in a line"

WHAT. Imagine three items — the numbers 3, 2, 1 — sitting in a row. Nothing clever yet: just boxes, left to right, plus one invisible box at the very end marked "nothing here".

WHY. Every loop is really asking one question over and over: "what's the next box, and is there a next box at all?" Before we can build machinery, we must see the thing the machinery walks across. The "nothing here" box at the end is the star of the whole story — that is where the loop learns to stop.

PICTURE. Look at the red box on the far right — it is not a number, it is the end marker. Every other box holds a real value.


Step 2 — The player has exactly ONE button: next

WHAT. We wrap the sequence in a little machine. The machine remembers one thing only: a pointer to "where I am right now." It exposes a single button labelled next. Press it → the machine hands you the box it's pointing at, then slides its pointer one step right.

WHY. Why only one button, why not "give me item #5 directly"? Because we want the same machine to work for lists, files, live sensor streams, and infinite sequences — and a file or a stream has no #5 you can jump to; you can only ever ask "what comes next?". Restricting ourselves to one forward button is exactly what makes the idea universal. This machine is the iterator.

PICTURE. The red arrow is the machine's internal pointer. Watch it: it starts on 3, and each next press nudges it rightward.

In code, "press the button" is the built-in next(it), which is just Python calling it.__next__() for you:


Step 3 — Pressing past the end makes a click: StopIteration

WHAT. Keep pressing. After handing you 3, 2, 1, the pointer arrives at the red end-marker box. Press next once more — there is nothing to hand over. So the machine does not return a value and does not return None. It raises StopIteration: a signal, a click, meaning "tape is finished."

WHY. Why a raised signal and not, say, returning None? Because None could be a real item in your sequence — [1, None, 2] is a perfectly good list. A returned value can never unambiguously mean "the end," because any value might genuinely be in the data. Raising an exception lives on a completely separate channel from returned values, so it can never be confused with a real item. That is the whole reason Python uses an exception here.

PICTURE. The pointer is now on the red end box. The red burst is the StopIteration click leaving the machine.


Step 4 — Where does the player come from? iter(obj)

WHAT. We never asked: who builds the machine? The row of boxes (the "bag of candy") is not itself the player. You hand the bag to the built-in iter(...), and it hands you back a brand-new player, pointer parked at the very first box.

WHY. Why separate the bag from the player? So you can pull out two independent players from the same bag — each with its own pointer. If the bag were the player (one shared pointer), two loops over the same data would fight over one position and nested loops would break. The thing that can produce a player is the iterable.

PICTURE. One bag (the boxes) on the left. iter() is the funnel. Out comes a red player with its pointer freshly set to the first box.


Step 5 — A player that is its own bag: __iter__ returns self

WHAT. Sometimes the player and the bag are the same object. When you already hold a player and you call iter() on it, it should just hand back itself — no new machine needed. So an iterator's __iter__ does one thing: return self.

WHY. Why must a player answer iter() at all? Because for always starts by calling iter(...) on whatever you gave it (Step 7). If you pass it a raw player, iter() must succeed and give a player back. Returning self makes the player pass this test without duplicating state.

PICTURE. The red loop-arrow: calling iter() on a player points right back at the same player.

class Countdown:
    def __init__(self, start):
        self.current = start      # the only state: where we are
    def __iter__(self):
        return self               # I am my own player  ← Step 5
    def __next__(self):
        if self.current <= 0:     # pointer past the end? ← Step 3
            raise StopIteration   # emit the click
        value = self.current      # the box under the pointer ← Step 2
        self.current -= 1         # slide pointer right
        return value              # hand it over
Recall Why check

self.current <= 0 before touching value? Order matters ::: We must decide "are we past the end?" before we hand anything back. If we returned first and checked after, we would hand out 0 (an item that shouldn't exist). Exhaustion check always comes first.


Step 6 — Edge case: the EMPTY sequence

WHAT. What if the bag has zero items — Countdown(0), or iter([])? The player is born with its pointer already on the red end-marker. The very first next press produces the click immediately. Zero values come out.

WHY. A correct loop must survive "nothing to do" without special-casing it. Because the exhaustion check runs first (Step 5), the empty case needs no extra code — the machinery handles it automatically. This is the sign of a good design: the degenerate case falls out for free.

PICTURE. Only the red end box exists. The pointer starts on it. First press → click.


Step 7 — Assemble the machine: what for actually is

WHAT. Now we stack every piece. A for loop is not a new idea — it is Steps 4, 2, and 3 glued together:

  1. Call iter(obj) once → get a player (Step 4).
  2. Loop forever: press next (Step 2).
  3. Got a value? Run the body.
  4. Got a StopIteration click? break and finish (Step 3).

WHY. Seeing the desugared form removes all magic. for knows nothing about lists, files, or your class — it only knows how to press a button and listen for the click. Everything loops uniformly because everything speaks this one tiny protocol.

PICTURE. The flow: iter at the top, the press-loop in the middle (red = the StopIteration escape edge), break at the exit.

iter

got value

StopIteration

obj (iterable)

it (iterator)

press next

run body

break and finish


The one-picture summary

Every earlier figure lives inside this one. The bag becomes a player via iter; the loop presses next and each press hands over a box while the pointer slides; the final press hits the red end and the StopIteration click routes the flow out to break.

Recall Feynman: tell it to a 12-year-old

You have a bag of candy (the list). You can't eat from the bag — first you pull out a little Pez dispenser and fill it: that's what iter() does, it hands you a fresh dispenser pointed at the first candy. Now the game: your friend (the for loop) keeps pressing the top — that's next — and one candy pops out each time, which your friend eats (runs the loop body). Press, eat, press, eat. When the dispenser is empty and your friend presses again, it makes a tiny click — that's StopIteration — and your friend knows to stop asking and walk away. If the bag was empty to begin with, the very first press clicks immediately and your friend just leaves — no crash, no fuss. And because the dispenser is separate from the bag, two friends can each pull their own dispenser from the same bag and press independently. That is the entire secret of every for loop in Python.


Connections

  • Generators — yield, lazy evaluation — a generator builds this exact machine for you automatically.
  • for loops and comprehensions — the desugaring in Step 7 is this note.
  • itertools — islice, count, chain — safe tools for infinite players (never fill a bag from one).
  • Lazy evaluation and memory efficiency — one candy at a time, never the whole bag in memory.
  • Dunder (magic) methods__iter__/__next__ are members of this family.
  • StopIteration and PEP 479 — why generators must return instead of raising the click by hand.