Python Course
Iterators
Every time you write a for loop in Python, something is happening behind the scenes that most beginners never think about. Python is not simply stepping through a list — it is asking an object for its next value, one at a time, using a protocol built into the language itself. That protocol is the iterator protocol, and understanding it changes the way you think about loops, data, and memory.
This lesson unpacks exactly how iteration works, shows you the built-in tools that leverage it, and teaches you to build your own iterable objects from scratch.
Iterables vs Iterators — The Key Distinction
These two terms are related but not the same, and confusing them is one of the most common sources of bugs for intermediate Python developers.
- An iterable is any object you can loop over — lists, tuples, strings, dictionaries, sets, files, and ranges are all iterables. An iterable knows how to produce an iterator when asked.
- An iterator is an object that does the actual work of stepping through values one at a time. It has an internal state that tracks where it is, and it produces the next value on demand.
- Every iterator is also an iterable — but not every iterable is an iterator.
# The difference between an iterable and an iterator
nums = [10, 20, 30] # this is an ITERABLE — it can produce an iterator
# iter() asks the iterable for its iterator
it = iter(nums) # now 'it' is the ITERATOR
# next() asks the iterator for the next value
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
# One more call raises StopIteration — nothing left
# print(next(it)) # would raise StopIteration20
30
iter(obj)callsobj.__iter__()and returns the iteratornext(it)callsit.__next__()and returns the next value- When values are exhausted,
next()raisesStopIteration— this is the signal Python uses to end aforloop - A
forloop is literally doing this behind the scenes on every iteration
How a for Loop Really Works
Now that you know about iter() and next(), you can see exactly what Python does when it executes a for loop. This mental model is essential for understanding why certain things behave the way they do.
# What Python does internally for every for loop
items = ["a", "b", "c"]
# What you write:
for item in items:
print(item)
print("---")
# What Python actually does:
_it = iter(items) # step 1 — get the iterator
while True:
try:
item = next(_it) # step 2 — get next value
print(item) # step 3 — run the loop body
except StopIteration:
break # step 4 — stop when exhaustedb
c
---
a
b
c
- Both versions produce identical output — the
forloop is syntactic sugar for this exact pattern StopIterationis not an error — it is the normal, expected signal that iteration is complete- This model explains why you cannot loop over a plain integer — it has no
__iter__method
The Iterator Protocol — __iter__ and __next__
Any object can become an iterator by implementing two special methods. This is called the iterator protocol.
__iter__(self)— returns the iterator object itself (usually justreturn self)__next__(self)— returns the next value, or raisesStopIterationwhen done
Why it exists: by standardizing how objects expose their values one at a time, Python lets any object — not just lists — work seamlessly in for loops, list(), sum(), zip(), and every other place that consumes sequences.
Real-world use: a database cursor object implements the iterator protocol so you can loop over query results row by row without loading the entire result set into memory at once.
# Building a custom iterator from scratch
class Countdown:
"""Counts down from a given start number to 1."""
def __init__(self, start):
self.current = start # track where we are
def __iter__(self):
return self # the object is its own iterator
def __next__(self):
if self.current <= 0:
raise StopIteration # signal that we are done
val = self.current
self.current -= 1 # advance internal state
return val
# Use it in a for loop — works like any built-in iterable
for n in Countdown(5):
print(n, end=" ")
print()
# Also works with list(), sum(), etc.
print(list(Countdown(4)))
print(sum(Countdown(4)))[4, 3, 2, 1]
10
- Implementing
__iter__and__next__is all it takes to make any class iterable - The iterator holds its own state — multiple independent iterators on the same data do not interfere
- Once exhausted, this iterator cannot be reused — create a new instance to iterate again
Separating the Iterable from the Iterator
A common and cleaner design is to have one class represent the data (the iterable) and a separate class handle the traversal (the iterator). This lets you create multiple independent iterators over the same data at the same time.
# Separate iterable and iterator classes
class NumberRange:
"""The iterable — holds the data."""
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
return NumberRangeIterator(self) # return a fresh iterator
class NumberRangeIterator:
"""The iterator — handles traversal."""
def __init__(self, source):
self.current = source.start
self.end = source.end
def __iter__(self):
return self
def __next__(self):
if self.current > self.end:
raise StopIteration
val = self.current
self.current += 1
return val
r = NumberRange(1, 4)
# Two independent iterators over the same range
it1 = iter(r)
it2 = iter(r)
print(next(it1)) # 1 — it1 is at position 1
print(next(it1)) # 2 — it1 advances
print(next(it2)) # 1 — it2 is independent, still at 1
# A for loop creates its own fresh iterator automatically
for n in r:
print(n, end=" ")2
1
1 2 3 4
- When
__iter__returnsself, the object is both iterable and iterator — but it can only be traversed once - When
__iter__returns a fresh iterator object, you can create multiple independent traversals - Python's built-in types like
listfollow this pattern —iter(mylist)always returns a fresh iterator
Built-in Functions That Use Iterators
Once you understand the iterator protocol, you realize that most of Python's built-in functions work with any iterator — not just lists. This is what makes Python so composable.
# Built-in functions that consume any iterator
class Evens:
"""Yields even numbers from 2 up to a limit."""
def __init__(self, limit):
self.n = 2
self.limit = limit
def __iter__(self): return self
def __next__(self):
if self.n > self.limit:
raise StopIteration
val = self.n
self.n += 2
return val
evens = Evens(10)
print(list(evens)) # convert to list — [2, 4, 6, 8, 10]
evens = Evens(10)
print(sum(evens)) # sum all values — 30
evens = Evens(10)
print(max(evens)) # largest value — 10
evens = Evens(6)
print(list(enumerate(evens))) # pair each with an index30
10
[(0, 2), (1, 4), (2, 6)]
list(),tuple(),set(),sum(),min(),max(),sorted(),enumerate(),zip()— all accept any iterator- Each call consumes the iterator — create a new instance if you need to iterate again
- This is why generators (next lesson) are so powerful — they are iterators too
iter() with a Sentinel Value
There is a two-argument form of iter() that is less well known but extremely useful. iter(callable, sentinel) calls the callable repeatedly until it returns the sentinel value, then stops — no class or __next__ needed.
Real-world use: reading a file in fixed-size chunks until an empty bytes object signals the end of the file.
# iter() with a sentinel — call a function until a value is returned
import random
random.seed(42) # fixed seed so output is predictable
# Roll a die repeatedly until we get a 6
roller = iter(lambda: random.randint(1, 6), 6)
rolls = list(roller) # collect all rolls before the first 6
print("Rolls before 6:", rolls)iter(callable, sentinel)creates an iterator that callscallable()each timenext()is invoked- When the callable returns a value equal to the sentinel,
StopIterationis raised automatically - The sentinel value itself is never included in the results
Summary Table
| Concept | What It Is | Key Method / Tool |
|---|---|---|
| Iterable | Object that can produce an iterator | __iter__() |
| Iterator | Object that yields values one at a time | __iter__() + __next__() |
iter() |
Gets the iterator from an iterable | Built-in function |
next() |
Retrieves the next value from an iterator | Built-in function |
StopIteration |
Signal that iteration is complete | Raised by __next__ |
| Iterator protocol | Contract any iterable class must fulfill | __iter__ + __next__ |
| Sentinel form | Calls a function until a value is hit | iter(callable, sentinel) |
Practice Questions
Practice 1. What built-in function do you call to get an iterator from an iterable?
Practice 2. What exception does __next__ raise to signal that there are no more values?
Practice 3. What are the two special methods an object must implement to satisfy the iterator protocol?
Practice 4. What does the two-argument form iter(callable, sentinel) do?
Practice 5. Is every iterator also an iterable?
Quiz
Quiz 1. What does Python do internally at the start of every for loop?
Quiz 2. What is the difference between an iterable and an iterator?
Quiz 3. If you call next() on an exhausted iterator, what happens?
Quiz 4. Why does separating the iterable class from the iterator class allow multiple independent traversals?
Quiz 5. Which of the following built-in functions does NOT work with a custom iterator?
Next up — Generators shows you a far simpler way to build iterators using the yield keyword, and explains why they are the preferred tool for working with large or infinite sequences.