Python Iterators Explained: How the Protocol Works
Python iterators power every for loop in the language. Learn how __iter__ and __next__ work, build a custom iterator class, and avoid the interview mistakes.
Every for loop in Python runs on two methods you probably never type directly: __iter__ and __next__.
That’s the iterator protocol. Understanding it separates engineers who use Python from engineers who understand Python. It’s also a frequent coding-round topic at product-focused companies. This article covers the protocol, shows you how to build a custom iterator class, and flags the mistakes that cost marks in interviews.
What “iterable” and “iterator” actually mean
These two words get used interchangeably in most tutorials. They’re not the same thing, and the distinction matters in an interview.
An iterable is any object that can produce an iterator. Lists, strings, tuples, dicts, and sets are all iterables. The formal test: if an object has an __iter__ method, it’s an iterable.
An iterator is the stateful object that __iter__ returns. It has a __next__ method that advances through the sequence one element at a time. Once exhausted, it stays exhausted. There is no rewind.
The Python 3 glossary defines an iterator as “an object representing a stream of data.” That stream flows one way. To traverse a sequence again from the beginning, call iter() on the original iterable to get a fresh iterator object.
Here is the distinction in code:
my_list = [10, 20, 30]
it = iter(my_list)
print(type(my_list)) # <class 'list'>
print(type(it)) # <class 'list_iterator'>
my_list is an iterable. it is the iterator. They are separate objects with different responsibilities.
The table below shows common types and which category they fall into:
| Type | Is iterable? | Is iterator? |
|---|---|---|
list | Yes | No |
str | Yes | No |
dict | Yes | No |
iter(list) | Yes | Yes |
iter(str) | Yes | Yes |
| Generator object | Yes | Yes |
Generators are iterators because they implement both dunder methods. Plain lists are not iterators because they have __iter__ but not __next__.
The iterator protocol: __iter__ and __next__
According to the Python 3 documentation on iterator types, every iterator must implement exactly two methods:
__iter__(): returns the iterator object itself (almost always justreturn self)__next__(): returns the next item in the sequence; raisesStopIterationwhen the sequence is done
The built-in functions iter() and next() are the public wrappers for those dunder methods. Calling iter(obj) calls obj.__iter__(). Calling next(it) calls it.__next__().
Here is the protocol in action on a plain list:
my_list = [3, 7, 5, 9, 2]
it = iter(my_list)
print(next(it)) # 3
print(next(it)) # 7
print(next(it)) # 5
print(next(it)) # 9
print(next(it)) # 2
# next(it) now raises StopIteration
When Python runs for x in my_list:, it does exactly this behind the scenes:
# What Python's for loop actually does
_iterator = iter(my_list)
while True:
try:
x = next(_iterator)
# run loop body here
except StopIteration:
break
The StopIteration exception is the termination contract. The for loop catches it and exits cleanly. If you call next() directly in your own code, you need to handle it yourself:
it = iter([1, 2])
print(next(it)) # 1
print(next(it)) # 2
print(next(it, "done")) # "done" (default value, no exception raised)
The optional second argument to next() is a default value returned instead of raising StopIteration. It’s a clean way to handle exhaustion without a try/except block.
Building a custom iterator class
Class-based iterators put the protocol to work as a design tool. The structure is fixed: __init__ for setup, __iter__ to initialise state and return self, __next__ to advance and either return the next value or raise StopIteration.
Here is a concrete example that generates even numbers from 0 up to a given maximum:
class EvenNumbers:
def __init__(self, max_val):
self.max_val = max_val
def __iter__(self):
self.current = 0
return self
def __next__(self):
if self.current <= self.max_val:
value = self.current
self.current += 2
return value
raise StopIteration
evens = EvenNumbers(10)
for n in evens:
print(n)
# Output: 0 2 4 6 8 10
Three things worth noting here:
__iter__resetsself.currentto 0. This is why usingfortwice on the sameEvenNumbersinstance works correctly: each loop cycle calls__iter__first, which resets the counter.- The boundary condition is
self.current <= self.max_val. Trace this carefully: whenself.currentis 10, the condition holds, so 10 is returned andself.currentadvances to 12. On the next call, 12 is not<= 10, soStopIterationis raised. Output: 0, 2, 4, 6, 8, 10. StopIterationis raised directly, not via areturn None. ReturningNonewould produce aNonevalue in the sequence, which is almost certainly not what you want.
A second example, closer to the kind of “multiply elements by a factor” question that appears in coding rounds:
class MultiplyByTwo:
def __init__(self, max_count):
self.max_count = max_count
def __iter__(self):
self.num = 0
return self
def __next__(self):
if self.num <= self.max_count:
result = self.num * 2
self.num += 1
return result
raise StopIteration
for val in MultiplyByTwo(8):
print(val)
# Output: 0 2 4 6 8 10 12 14 16
Trace: self.num runs from 0 to 8 inclusive (9 values), each multiplied by 2. When self.num reaches 9, 9 <= 8 is False, so StopIteration is raised. Nine output values: 0, 2, 4, 6, 8, 10, 12, 14, 16. The arithmetic checks out.
Infinite iterators and how to stop them
An infinite iterator never raises StopIteration on its own. It generates values on demand indefinitely, which is useful for sequences that have no natural end.
class CountUp:
def __iter__(self):
self.n = 0
return self
def __next__(self):
self.n += 1
return self.n
Running for x in CountUp(): without a break will loop until the process is killed. Two practical ways to stop an infinite iterator:
- Add an explicit
breakinside theforbody:
counter = CountUp()
for val in counter:
if val > 5:
break
print(val)
# Prints: 1 2 3 4 5
- Use
itertools.isliceto cap the number of items drawn:
import itertools
counter = CountUp()
first_five = list(itertools.islice(counter, 5))
print(first_five)
# [1, 2, 3, 4, 5]
itertools.islice calls next() exactly as many times as you specify, then stops. The iterator itself has no idea it was capped. This pattern appears in production code whenever you want to process a fixed batch from a potentially unlimited stream.
For standard counting sequences, prefer itertools.count over a hand-rolled class:
import itertools
for n in itertools.islice(itertools.count(0, 2), 6):
print(n)
# 0 2 4 6 8 10
The standard library version is faster, better tested, and immediately recognisable to anyone reading your code.
Iterator protocol in technical interviews
Iterator-protocol questions appear in coding rounds in two forms.
The first is conceptual: “Explain how Python’s for loop works under the hood.” The answer comes back to the four steps: call iter() on the container, call next() on the iterator each cycle, catch StopIteration to exit, and note that the iterator maintains position while the original iterable does not.
The second is implementation: “Write a class that behaves like an iterator for this sequence.” The answer structure is always the same: __init__ for parameters, __iter__ to reset and return self, __next__ to advance and raise when done.
The off-by-one trap is where most candidates lose marks. Given a class with a condition like self.num <= self.max, trace the boundary manually before finalising the code: what happens when self.num equals self.max? Is the last value returned or skipped? Step through it.
If you’ve studied Java interview questions on iterators and collections, the Python version maps cleanly. Java’s Iterator<T> interface uses hasNext() and next() rather than StopIteration, but the design reasoning is identical: one method checks availability, one method advances. In Python, the check and the advance collapse into a single __next__ call that either returns a value or raises. That collapse is what makes Python iterators simpler to implement but slightly trickier to reason about at boundaries. A broader set of Java and Python coding-round patterns follows the same structural logic.
One distinction worth making explicit in interviews: don’t confuse iterators with generators when asked to build a class. A generator function uses yield and Python fills in __iter__ and __next__ for you. A class-based iterator implements both dunder methods explicitly. If an interviewer asks you to build a class, they want to see the protocol mechanics directly, not a generator shortcut.
The same pattern that makes a custom iterator work in a placement round also appears in production data pipelines. Streaming API responses from large language models return tokens one at a time using exactly this iterator-style protocol. At TinkerLLM, ₹299 gets you live LLM API calls where you can write Python code that reads streamed responses in a loop, the __next__ pattern applied to real output rather than a contrived coding-round list. The iterator class you write for an interview and the code you write to process a streamed model response are solving the same underlying problem.
Primary sources
Frequently asked questions
What is the difference between an iterator and an iterable in Python?
An iterable is any object with an __iter__ method, such as a list, string, or dict. An iterator is the stateful object __iter__() returns, which has a __next__ method that advances one element at a time.
Can I iterate over the same iterator twice?
No. Once an iterator is exhausted, calling next() raises StopIteration permanently. To iterate again, call iter() on the original iterable to get a fresh iterator object.
What exception does Python raise when an iterator is exhausted?
StopIteration. The for loop catches this exception automatically and exits cleanly. You can also catch it manually with a try/except block when calling next() directly.
How is an iterator different from a generator in Python?
A generator is a function using the yield keyword that produces an iterator automatically. Every generator is an iterator, but not every iterator is a generator. Class-based iterators give explicit control over internal state.
Do placement companies ask about Python iterators in coding rounds?
Yes. Product-focused companies ask candidates to build custom iterator classes as part of object-oriented design questions. They test whether you understand __iter__ and __next__ and can trace StopIteration correctly through edge cases.
A self-paced playground for building with LLMs.
TinkerLLM is FACE Prep's sister property. A guided environment for shipping real LLM applications, the kind of project that earns a paragraph on your resume, not a line.
Try TinkerLLM (₹299 launch)