Iterators & the Iterator Protocol Interview Questions & Answers
6 questions Updated 2026-06-18
Python interview questions on iterables vs iterators, the iterator protocol, __iter__ and __next__, StopIteration, building custom iterators, and how for loops work under the hood.
An iterable is anything you can loop over — it knows how to produce an
iterator via __iter__. An iterator is the object that actually does
the walking: it has __next__ and yields one value at a time, remembering
its position. Every iterator is iterable (its __iter__ returns itself),
but not every iterable is an iterator.
nums = [1, 2, 3] # list: iterable, NOT an iterator
it = iter(nums) # iterator over the list
next(it) # 1 — iterators track position
next(it) # 2
Think of the iterable as the collection and the iterator as a cursor/bookmark into it. You can create many independent iterators from one iterable.
The iterator protocol is two methods. __iter__ must return the
iterator object itself, and __next__ returns the next value or raises
StopIteration when exhausted. That exception is the agreed signal
that there are no more items.
it = iter([10, 20])
it.__next__() # 10
it.__next__() # 20
it.__next__() # raises StopIteration
An iterable only needs __iter__ (returning a fresh iterator). An
iterator needs both. The StopIteration raise is what lets for loops
know when to stop — they catch it silently.
iter(obj) calls obj.__iter__() to get an iterator; next(it) calls
it.__next__() to advance it. next() accepts an optional default that
is returned instead of raising StopIteration when the iterator is
exhausted — handy for safe peeking.
it = iter("ab")
next(it) # 'a'
next(it) # 'b'
next(it, "done") # 'done' — default instead of StopIteration
# iter() also has a two-arg sentinel form:
# iter(callable, sentinel) calls until it returns sentinel
Use the default argument whenever you want to drain or sample an iterator
without wrapping next() in a try/except StopIteration.
Implement __iter__ (return self) and __next__ (return the next value
or raise StopIteration). The instance holds its own state between calls.
class Countdown:
def __init__(self, start):
self.n = start
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
list(Countdown(3)) # [3, 2, 1]
This works, but for most cases a generator function (using yield) is
far less boilerplate — it builds the __iter__/__next__/StopIteration
machinery for you. Reach for a class only when you need extra methods or
explicit state.
A for loop is sugar over the iterator protocol. Python calls iter() on
the iterable once to get an iterator, then repeatedly calls next() on it,
binding each result to the loop variable, until StopIteration is raised —
which it catches to end the loop.
for x in [1, 2, 3]:
print(x)
# is roughly equivalent to:
_it = iter([1, 2, 3])
while True:
try:
x = next(_it)
except StopIteration:
break
print(x)
This is why any object implementing the protocol "just works" in a for
loop, comprehension, or *-unpacking. The StopIteration is the hidden
handshake that terminates the loop.
An iterator is single-use / exhaustible: once __next__ has walked to
the end and raised StopIteration, it stays exhausted — there is no reset.
Re-iterating yields nothing. This trips people up with generators and
zip/map objects.
it = iter([1, 2, 3])
list(it) # [1, 2, 3]
list(it) # [] — already exhausted!
gen = (x for x in range(3))
sum(gen) # 3
sum(gen) # 0 — the generator is spent
A list (an iterable, not an iterator) can be looped many times because
each loop calls iter() to get a fresh iterator. If you need to reuse an
exhaustible result, materialize it into a list first.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.