Overview
When working with Python, it is easy to iterate over lists, tuples, strings, and many other objects using a for loop. Although the process appears simple, Python follows a set of rules behind the scenes to retrieve each element one at a time.
These rules are known as the Python iterator protocol. They also make it possible to create custom iterators that define how values are generated and returned during iteration.
In this tutorial, you’ll learn what the Python iterator protocol is, how the __iter__() and __next__() methods work together, how to create custom iterators, and how Python uses them automatically in for loops.
Quick Navigation
You can quickly navigate to any section of this guide using the links below:
- What Is the Python Iterator Protocol?
- How the Python Iterator Protocol Works
- The
__iter__()Method - The
__next__()Method - Creating a Custom Iterator
- How
forLoops Work with Iterators - Iterator vs Generator
- Examples: Python Iterator Protocol
- Common Beginner Mistakes: Iterator Protocol
- Practical Use Cases: Iterator Protocol
- Key Takeaways: Iterator Protocol
Introduction: What Is the Python Iterator Protocol?
The Python iterator protocol is a set of rules that tells Python how to retrieve elements from an object one at a time.
It is built around two special methods:
__iter__()returns an iterator.__next__()returns the next available element.
Whenever Python loops through a list, tuple, string, dictionary, set, file, generator, or a custom iterator, it follows these rules automatically.
Understanding the iterator protocol also makes it easier to understand how iter(), next(), generators, and for loops work together.
Before creating custom iterators, let’s first understand how the Python iterator protocol works behind the scenes.
How the Python Iterator Protocol Works
Every time Python starts iterating over an object, it follows the iterator protocol automatically.
The process works in the following order:
- Python calls the
iter()function on an iterable. - The object’s
__iter__()method returns an iterator. - Python repeatedly calls the
next()function. - The iterator’s
__next__()method returns one value at a time. - When no values remain,
__next__()raises aStopIterationexception. - Python stops the iteration.
Iterator Protocol Flow
Iterable
│
▼
iter()
│
▼
__iter__()
│
▼
Iterator
│
▼
next()
│
▼
__next__()
│
▼
Next Value
│
▼
Repeat
│
▼
StopIteration
│
▼
Iteration Ends
In simple terms, iter() starts the iteration, while next() keeps retrieving values until the iterator has no more elements.
The __iter__() Method in Python
The __iter__() method is the starting point of the Python iterator protocol. Its purpose is to return an iterator.
Python automatically calls this method whenever the iter() function is used or when a for loop begins iterating over an object.
In many custom iterator classes, the object itself acts as the iterator, so the __iter__() method simply returns self.
Simple Example
class Numbers:
def __iter__(self):
return self
Here, __iter__() returns the current object, allowing Python to use it as an iterator.
The __next__() Method in Python
The __next__() method returns the next value during iteration.
Each time Python calls the next() function, it actually executes the iterator’s __next__() method.
When there are no more values to return, the method raises a StopIteration exception to tell Python that the iteration has finished.
Simple Example
class Counter:
def __init__(self):
self.number = 1
def __next__(self):
value = self.number
self.number += 1
return value
Each call to __next__() returns the current value and prepares the next value for the following iteration.
Creating a Custom Iterator in Python
Python also lets you create your own iterators when the built-in ones are not enough.
A custom iterator must implement both the __iter__() and __next__() methods.
Basic Structure
class MyIterator:
def __iter__(self):
return self
def __next__(self):
# Return the next value
# Raise StopIteration when finished
The __iter__() method returns the iterator, while __next__() controls what value should be returned each time Python requests the next element.
The upcoming Python Iterator Protocol Examples show how these two methods work together to build complete custom iterators.
How Python for Loops Work with Iterators
Behind the scenes, Python performs these steps automatically:
- Calls
iter()to obtain an iterator. - Calls
next()repeatedly to retrieve each value. - Uses the iterator’s
__next__()method to return the next element. - Stops the loop when
StopIterationis raised.
for Loop Workflow
The following diagram shows how these steps are connected during iteration.
for Loop
│
▼
iter()
│
▼
__iter__()
│
▼
next()
│
▼
__next__()
│
▼
Value Returned
│
▼
Repeat
│
▼
StopIteration
│
▼
Loop Ends
This is why you normally do not need to call iter() or next() manually when using a for loop.
Iterator vs Generator in Python
Both iterators and generators return one value at a time, but they are created in different ways.
| Iterator | Generator |
|---|---|
| Created using a class. | Created using a function with yield. |
Requires __iter__() and __next__(). |
Python creates these methods automatically. |
| Usually requires more code. | Usually requires less code. |
| Provides complete control over iteration. | Provides a simpler way to generate values. |
Use a custom iterator when you need full control over the iteration process. If you simply want to generate values one at a time with less code, a generator is often the better choice.
Now that you understand the iterator protocol, let’s see how these concepts are applied through practical examples.
Examples: Python Iterator Protocol
Now that you understand how the iterator protocol works, let’s see how these concepts are used in real Python programs. Each example introduces one new idea, making it easier to understand how custom iterators behave during iteration.
Example 1: Create a Basic Custom Iterator
This example creates a simple custom iterator that returns three numbers before stopping.
class Numbers:
def __init__(self):
self.number = 1
def __iter__(self):
return self
def __next__(self):
if self.number <= 3:
value = self.number
self.number += 1
return value
raise StopIteration
iterator = Numbers()
print(next(iterator))
print(next(iterator))
print(next(iterator))
# Output
1
2
3
Explanation: The __iter__() method returns the iterator, while __next__() returns one number at a time. After returning 3, it raises StopIteration, indicating that no more values are available.
Example 2: Create a Counting Iterator
A custom iterator can also generate a sequence of numbers instead of storing them in a collection.
class Counter:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current <= self.end:
value = self.current
self.current += 1
return value
raise StopIteration
counter = Counter(5, 8)
for number in counter:
print(number)
# Output
5
6
7
8
Explanation: The iterator generates numbers from 5 to 8. Each call to __next__() returns the current number and prepares the next one until the limit is reached.
Example 3: Iterate Over Characters in a String
A custom iterator can also work with strings by returning one character at a time.
class StringIterator:
def __init__(self, text):
self.text = text
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index < len(self.text):
character = self.text[self.index]
self.index += 1
return character
raise StopIteration
letters = StringIterator("Python")
for letter in letters:
print(letter)
# Output
P
y
t
h
o
n
Explanation: Instead of returning numbers, this iterator returns one character from the string during each iteration. Once every character has been processed, the iterator raises StopIteration and the for loop ends automatically.
Example 4: Create an Iterator with a Limit
This example creates a custom iterator that returns only a fixed number of values.
class LimitedCounter:
def __init__(self, limit):
self.current = 1
self.limit = limit
def __iter__(self):
return self
def __next__(self):
if self.current <= self.limit:
value = self.current
self.current += 1
return value
raise StopIteration
numbers = LimitedCounter(5)
for number in numbers:
print(number)
# Output
1
2
3
4
5
Explanation: The iterator returns numbers from 1 up to the specified limit. Once the limit is reached, __next__() raises StopIteration, ending the iteration.
Example 5: Create an Infinite Iterator
A custom iterator can also generate values continuously. In practice, you should always provide a condition to stop the iteration.
class InfiniteCounter:
def __init__(self):
self.number = 1
def __iter__(self):
return self
def __next__(self):
value = self.number
self.number += 1
return value
counter = InfiniteCounter()
for _ in range(5):
print(next(counter))
# Output
1
2
3
4
5
Explanation: This iterator never raises StopIteration, so it can continue generating values indefinitely. Here, the range(5) loop limits the output to the first five numbers.
Example 6: Use a Custom Iterator in a for Loop
Once a class follows the iterator protocol, it can be used directly in a for loop.
class EvenNumbers:
def __init__(self):
self.number = 2
def __iter__(self):
return self
def __next__(self):
if self.number <= 10:
value = self.number
self.number += 2
return value
raise StopIteration
for number in EvenNumbers():
print(number)
# Output
2
4
6
8
10
Explanation: The for loop automatically calls __iter__() once and then repeatedly calls __next__(). When StopIteration is raised, the loop ends without requiring any additional code.
Common Beginner Mistakes: Iterator Protocol
While learning the Python iterator protocol, beginners often make a few common mistakes. Avoiding them will help you create custom iterators correctly.
- Forgetting to implement
__iter__(). Every custom iterator should provide an__iter__()method that returns an iterator. - Not raising
StopIteration. When no values remain,__next__()should raise this exception to end the iteration. - Returning incorrect values from
__next__(). Each call should return the next value and update the iterator’s position. - Confusing iterators with generators. Iterators are usually created with classes, while generators are created using functions and the
yieldkeyword.
Practical Use Cases: Iterator Protocol
Here are some common use cases of the Python iterator protocol:
- Processing large datasets: Process data one item at a time to reduce memory usage.
- Streaming data: Read data as it becomes available from files, APIs, or network connections.
- Lazy evaluation: Generate values only when they are needed.
- Creating custom iterators: Define how your own objects should be iterated.
Key Takeaways: Iterator Protocol
Let’s quickly review the main concepts covered in this Python Iterator Protocol tutorial:
- The Python iterator protocol is built around the
__iter__()and__next__()methods. - The
iter()andnext()functions rely on these methods to perform iteration. - Custom iterators allow you to define exactly how values are produced and returned.
- Python
forloops automatically follow the iterator protocol behind the scenes. - Understanding the iterator protocol makes it easier to learn generators and other advanced iteration techniques in Python.