Python Iterators
Iterators
Every time you write a for loop in Python, an iterator is working behind the scenes. Understanding iterators explains how for loops actually work, why some objects can only be looped over once, and how to build your own objects that can be used in a for loop.
What Is an Iterator?
An iterator is an object that lets you move through a sequence of values one element at a time. It remembers its current position, and each time you ask for the next value, it returns that value and advances.
Iterators can produce values on demand instead of holding all of them in memory at once, which makes them efficient for large or even endless sequences.
The Iterator Protocol
In Python, an object is an iterator if it follows the iterator protocol — that is, if it implements two special methods:
| Method | Purpose |
|---|---|
__iter__() | Returns the iterator object itself |
__next__() | Returns the next value; raises StopIteration when no values remain |
The built-in functions iter() and next() call these methods for you.
Iterable vs Iterator
These two terms sound similar but mean different things:
| Term | Meaning | Examples |
|---|---|---|
| Iterable | An object you can loop over. It can produce an iterator. | Lists, tuples, strings, sets, dictionaries, ranges |
| Iterator | The object that does the looping — it tracks the position and returns values one by one. | The object returned by iter(my_list), file objects, generators |
An analogy: a book is iterable — it can be read. A bookmark is the iterator — it remembers which page you are on and moves forward as you read. You can have several bookmarks in the same book, each at a different page.
Getting an Iterator From an Iterable
Use the built-in iter() function to get an iterator from any iterable, and next() to retrieve values from it.
Example: Iterator From a List
fruits = ["apple", "orange", "mango"]
iterator = iter(fruits)
print(next(iterator))
print(next(iterator))
print(next(iterator))
Expected output:
apple
orange
mango
Each call to next() returns the next item and moves the iterator forward.
What Happens When the Values Run Out?
Calling next() again after the last item raises StopIteration:
fruits = ["apple", "orange", "mango"]
iterator = iter(fruits)
next(iterator)
next(iterator)
next(iterator)
next(iterator)
Output:
StopIteration
StopIteration is not really an error in normal use — it is the signal that the iterator is finished.
Example: Iterator From a String
Strings are iterable, character by character.
text = "python"
char_iter = iter(text)
print(next(char_iter))
print(next(char_iter))
print(next(char_iter))
Expected output:
p
y
t
The iterator still remembers its position: the next call to next(char_iter) would return h.
Iterators Are Used Up
An iterator moves only forward. Once it has produced all its values, it is exhausted:
numbers = [1, 2, 3]
it = iter(numbers)
print(list(it))
print(list(it))
Expected output:
[1, 2, 3]
[]
The second list(it) is empty because the iterator has already been consumed. The original list, however, is unchanged — calling iter(numbers) again creates a fresh iterator.
Looping Through an Iterable
In practice, you rarely call next() yourself. A for loop does it automatically.
Example: Looping Through a Set
colors = {"red", "blue", "green"}
for color in colors:
print(color)
Example output (set order may vary):
blue
green
red
Example: Looping Through a String
word = "code"
for letter in word:
print(letter)
Expected output:
c
o
d
e
How a for Loop Works Internally
Behind the scenes, a for loop:
- Calls
iter()on the iterable to get an iterator. - Calls
next()on the iterator repeatedly, assigning each value to the loop variable. - Stops quietly when
StopIterationis raised.
This for loop:
for letter in "code":
print(letter)
behaves like this while loop:
iterator = iter("code")
while True:
try:
letter = next(iterator)
except StopIteration:
break
print(letter)
Both print the same four letters. The for loop is simply a convenient way of writing the iterator protocol.
Creating Your Own Iterator
To make a custom iterator, define a class with the two protocol methods:
__iter__()— prepares the iterator and returnsself__next__()— returns the next value
Note: This section uses classes, which are covered in detail in the Python Classes and Objects lesson.
selfrefers to the object itself and is how the iterator stores its current state between calls.
Example: Custom Iterator for Even Numbers
This iterator produces even numbers starting from 2.
class EvenNumbers:
def __iter__(self):
self.num = 2
return self
def __next__(self):
value = self.num
self.num += 2
return value
even_iter = iter(EvenNumbers())
print(next(even_iter))
print(next(even_iter))
print(next(even_iter))
print(next(even_iter))
Expected output:
2
4
6
8
How it works:
iter(EvenNumbers())calls__iter__(), which sets the starting valueself.num = 2and returns the object.- Each
next()call runs__next__(), which saves the current number, increasesself.numby 2 for next time, and returns the saved number.
This iterator never runs out. Using it directly in a for loop would create an infinite loop.
Stopping Iteration With StopIteration
To make an iterator finite, raise StopIteration inside __next__() when there are no more values.
Example: Iterator With a Stop Condition
This iterator produces the squares of 1 through 5.
class SquareLimit:
def __iter__(self):
self.n = 1
return self
def __next__(self):
if self.n <= 5:
result = self.n ** 2
self.n += 1
return result
else:
raise StopIteration
squares = SquareLimit()
for value in squares:
print(value)
Expected output:
1
4
9
16
25
What happens:
- The
forloop callsiter(squares), which setsself.n = 1. - Each loop iteration calls
__next__(), which returns the square ofnand increasesn. - When
nbecomes 6,__next__()raisesStopIteration, and theforloop ends cleanly.
Making the Limit Configurable
Using __init__() (the method that runs when an object is created), you can pass the limit in:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for number in Countdown(3):
print(number)
Expected output:
3
2
1
Iterators vs Generators
Writing an iterator class requires __iter__(), __next__(), and manual state tracking. A generator function achieves the same result with much less code, because Python creates the iterator automatically:
def countdown(start):
while start > 0:
yield start
start -= 1
for number in countdown(3):
print(number)
Expected output:
3
2
1
Every generator is an iterator. Use a generator for most custom iteration needs, and an iterator class when you need extra methods or more complex state. Generators are covered in the Python Functions lesson.
Common Mistakes
| Mistake | Problem |
|---|---|
Calling next() on a list directly | TypeError: 'list' object is not an iterator — use iter() first |
| Looping over an exhausted iterator | Produces nothing |
Forgetting to raise StopIteration | Infinite iteration |
Forgetting return self in __iter__() | TypeError: iter() returned non-iterator |
Related Concepts
- Python for Loops — the main consumer of iterators
- Python Functions — generators and the
yieldkeyword - Python Classes and Objects — building custom iterator classes
- Python Range — an iterable that produces numbers on demand