Python Iterator Protocol: A Complete Beginner’s Guide with Examples

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:

💡 Tip: For a complete overview of Python iteration, including its uses, advantages, and related concepts, read our Python Iteration Tutorial.

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:

  1. __iter__() returns an iterator.
  2. __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:

  1. Python calls the iter() function on an iterable.
  2. The object’s __iter__() method returns an iterator.
  3. Python repeatedly calls the next() function.
  4. The iterator’s __next__() method returns one value at a time.
  5. When no values remain, __next__() raises a StopIteration exception.
  6. 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:

  1. Calls iter() to obtain an iterator.
  2. Calls next() repeatedly to retrieve each value.
  3. Uses the iterator’s __next__() method to return the next element.
  4. Stops the loop when StopIteration is 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.

  1. Forgetting to implement __iter__(). Every custom iterator should provide an __iter__() method that returns an iterator.
  2. Not raising StopIteration. When no values remain, __next__() should raise this exception to end the iteration.
  3. Returning incorrect values from __next__(). Each call should return the next value and update the iterator’s position.
  4. Confusing iterators with generators. Iterators are usually created with classes, while generators are created using functions and the yield keyword.

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() and next() functions rely on these methods to perform iteration.
  • Custom iterators allow you to define exactly how values are produced and returned.
  • Python for loops 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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top