1.3.10 · D5Python Intermediate

Question bank — Iterators — `__iter__`, `__next__`, StopIteration

1,815 words8 min readBack to topic

The three symbols, refreshed before we use them

Everything on this page leans on exactly three pieces of vocabulary. So that a reader landing here in isolation never meets a symbol undefined, here they are in plain words:

Keep the picture below in mind for every trap — the tape player (iterator) has one button (next), pressing past the end gives the click (StopIteration), and a list is a bag that hands you a fresh player each time you ask (iter).

Figure — Iterators — `__iter__`, `__next__`, StopIteration

The iterable vs iterator split — a bag versus the dispenser it produces — is the single idea that most traps below hinge on:

Figure — Iterators — `__iter__`, `__next__`, StopIteration

True or false — justify

Every iterable is also an iterator.
False. An iterable only promises __iter__ (it can give a dispenser); a list is iterable but has no __next__, so you cannot press its button directly — next([1,2]) raises TypeError. Iterators are the narrower kind that also answer next().
Every iterator is also an iterable.
True. By the protocol, an iterator must have __iter__ returning self, so for and iter() accept it directly.
An iterator's __iter__ must return a brand-new object.
False. It returns ==self== — literally the same object, so it.__iter__() is it is True. Returning a new one would abandon the current position and break the protocol.
A reusable iterable's __iter__ should return self.
False. A reusable iterable returns a fresh iterator each call, so obj.__iter__() is obj.__iter__() is False — each for loop gets its own independent position.
Calling iter() on a list twice gives two independent positions.
True. a = iter([1,2]); b = iter([1,2])a is b is False; advancing a with next(a) leaves b untouched. That independence is the whole reason iterable and iterator are separate roles.
Calling iter() on an iterator gives a new independent position.
False. it2 = iter(it)it2 is it is True, so both names share one cursor — next(it2) also advances it.
StopIteration is an error your code did something wrong to trigger.
False. It is the agreed end-of-tape signal; for deliberately wraps next() in try/except StopIteration: break. Nothing went wrong — the loop just heard the click.
Once an iterator raises StopIteration, pressing next() again resets it.
False. It stays exhausted. A well-behaved iterator keeps raising StopIteration forever; it never magically refills. To restart, you must build a new iterator.
An infinite iterator is impossible because you can't store infinite items.
False. __next__ computes one item on demand and stores only its current state (e.g. self.n), so it never needs the whole (infinite) sequence in memory.
next(it, default) and next(it) behave the same when the iterator is empty.
False. next(it) raises StopIteration; next(it, default) catches it internally and returns default instead — see the "why" below for the reason both forms exist.
You can loop over the same list twice and get all elements both times.
True. Each for calls iter(list) afresh, so a new cursor starts at the beginning each loop.
You can loop over the same iterator twice and get all elements both times.
False. The second loop starts from the already-exhausted position and yields nothing, because the single cursor was consumed.
An object with no __iter__ can never be looped over.
False. Python has a legacy fallback: if __iter__ is missing but __getitem__ exists, iter() builds an iterator that calls obj[0], obj[1], ... until IndexError, which it treats like StopIteration. See the edge-case section.

Spot the error

class C: with only __next__ and no __iter__ (and no __getitem__) — what breaks?
for/iter() call __iter__ first, and with no __getitem__ fallback either, you get TypeError: object is not iterable. Add __iter__ returning self.
In __next__, using return instead of raise StopIteration to stop.
return just hands back a value (often None); the loop never learns the tape ended and runs forever or yields Nones. You must raise StopIteration.
__next__ advances state before the exhaustion check, e.g. decrements then checks <= 0.
It hands back the boundary value (like 0) that should have been excluded. The exhaustion check must come first, before computing/returning.
__iter__ returns iter(self.data) in a class meant to be its own iterator with __next__.
Now for uses the data's iterator and never touches your __next__; your carefully written logic is silently bypassed. Return self if the object is its own iterator.
A class stores no reset logic, returns self from __iter__, and is looped twice expecting a full run each time.
The second loop finds the state already consumed (self.current at its end value) and yields nothing. For reuse, return a fresh iterator from __iter__ instead of self.
Inside a generator, doing raise StopIteration to end early.
Since PEP 479 that StopIteration becomes a RuntimeError when it escapes the generator. In generators use return; only hand-written classes raise StopIteration. See StopIteration and PEP 479.
A __next__ that computes the value but forgets to advance self.current.
It returns the same item forever — an accidental infinite loop of one repeated value. Every __next__ must move the state forward before or after returning.

Why questions

Why does for need try/except StopIteration rather than checking a "hasNext" method?
Python chose the exception as the single end signal so for works uniformly over every iterator without asking each one to also implement lookahead. The exception is the "no more" answer.
Why must an iterator's __iter__ return self and not, say, a copy?
So that passing an already-running iterator to for or iter() continues from where it is, rather than silently restarting or duplicating state.
Why does next(it, default) exist at all when next(it) already works?
Because sometimes "the tape is empty" is a normal answer, not an emergency. The two-argument form lets you say "give me one item, or this fallback" in a single expression — no try/except StopIteration boilerplate. The one-argument form raises so that loops and callers who do need to know about exhaustion aren't handed a silent sentinel by accident. You pick raising vs. defaulting based on whether emptiness is exceptional or expected.
Why separate "iterable" from "iterator" at all — why not merge them?
Merging forces one shared cursor. Then nested loops over the same object would clash: the inner loop exhausts the cursor and the outer loop never advances. Separation lets each loop own an independent position.
Why do generators feel simpler than writing __iter__/__next__ by hand?
A generator auto-implements the full protocol — __iter__ returns self, yield produces items, and hitting the end raises StopIteration for you. See Generators — yield, lazy evaluation.
Why is list(infinite_iterator) dangerous but next(infinite_iterator) fine?
list() keeps pressing next until StopIteration, which never comes, so it hangs. next() presses exactly once. Use itertools.islice to take a finite slice safely.
Why is laziness (one item at a time) the point, not a side effect?
It lets iterators represent streaming or infinite data using only current-state memory — the core motivation covered in Lazy evaluation and memory efficiency.

Edge cases

What does iter([]) then next() do?
iter([]) succeeds and returns an empty iterator; the first next() immediately raises StopIteration since there is nothing to hand back.
What does for x in EmptyIterable(): execute?
Zero body iterations. for gets the iterator, the first next raises StopIteration, and the loop breaks before running the body once.
If __next__ raises StopIteration on the very first call, is that valid?
Yes — it's a legitimately empty iterator. for simply runs its body zero times; nothing is wrong.
What happens if you call next() twice past the end of a finite iterator?
Both calls raise StopIteration; the iterator does not reset, refill, or start erroring differently. Exhausted stays exhausted.
Can the same object be both the iterable and the iterator?
Yes — when __iter__ returns self and __next__ exists on the same object. It's valid but single-use: it cannot be re-looped, since its one cursor is shared.
Is a dictionary iterable, and what does iterating it yield?
Yes. Its __iter__ yields the keys (not key–value pairs), each iter(dict) giving a fresh key-iterator with its own position.
A class with __getitem__ but no __iter__ — is it iterable?
Yes, via the legacy sequence protocol. iter() falls back to calling obj[0], obj[1], ... until IndexError, which it converts to StopIteration. This is how old-style sequences worked before __iter__ existed, and why some objects loop despite having no __iter__.

Recall One-line summary of the traps

Iterable = "gives a dispenser," iterator = "is the dispenser." __iter__ on an iterator returns self; on a reusable iterable it returns a fresh one. StopIteration is a signal, not a failure, it never resets, and generators must return instead of raising it. Missing __iter__ but has __getitem__? The legacy obj[0], obj[1], ... fallback still makes it iterable.


Connections

  • Iterators — `__iter__`, `__next__`, StopIteration (index 1.3.10) — the parent concept these traps drill.
  • for loops and comprehensions — where the try/except StopIteration machinery hides.
  • Generators — yield, lazy evaluation — the auto-implemented alternative.
  • itertools — islice, count, chain — safe handling of infinite iterators.
  • Lazy evaluation and memory efficiency — why one-at-a-time matters.
  • Dunder (magic) methods — the family __iter__/__next__ belong to.
  • StopIteration and PEP 479 — the generator return vs raise rule.