DEV Community

Cover image for The Conveyor Belt Protocol: Understanding Iterators
Aaron Rose
Aaron Rose

Posted on

The Conveyor Belt Protocol: Understanding Iterators

Timothy had been using for loops for months, never questioning how they worked. But when he tried to iterate over a custom data structure he'd built—a specialized book index—Python refused with a cryptic error: 'BookIndex' object is not iterable.

Margaret found him puzzling over the error message. "You've been using the conveyor belt system without understanding its mechanics," she said, leading him to the library's basement where massive brass gears and leather belts powered the book delivery system. "It's time to learn the Iterator Protocol—the contract that makes iteration work."

The Mystery of For Loops

Timothy had always taken for loops for granted:

# Note: Examples use placeholder functions like process_chunk(), add_book_to_catalog()
# In practice, replace these with your actual implementation

books = ["1984", "Dune", "Foundation"]
for book in books:
    print(book)
Enter fullscreen mode Exit fullscreen mode

"How does Python know how to iterate over a list?" Timothy asked. "And why doesn't it work with my BookIndex?"

Margaret showed him what Python did behind the scenes:

books = ["1984", "Dune", "Foundation"]

# What for loop actually does:
iterator = iter(books)  # Get an iterator from the iterable
while True:
    try:
        book = next(iterator)  # Get next item
        print(book)
    except StopIteration:  # No more items
        break
Enter fullscreen mode Exit fullscreen mode

"Every for loop," Margaret explained, "is syntactic sugar for this pattern. Python calls iter() on the object to get an iterator, then repeatedly calls next() until StopIteration is raised."

Iterables vs Iterators

Timothy learned the crucial distinction:

Iterable: An object that can produce an iterator (has __iter__ method)
Iterator: An object that produces values one at a time (has __next__ method)

books = ["1984", "Dune", "Foundation"]  # This is an ITERABLE

iterator = iter(books)  # This is an ITERATOR

print(type(books))     # <class 'list'> - iterable
print(type(iterator))  # <class 'list_iterator'> - iterator
Enter fullscreen mode Exit fullscreen mode

Margaret drew the analogy: "The iterable is like the library's card catalog—it contains information about books. The iterator is like a librarian walking through the stacks, fetching one book at a time."

The Memory Efficiency Advantage

Margaret showed Timothy why iterators mattered:

# Without iterators - creates entire list in memory
million_numbers = [i for i in range(1000000)]  # Uses ~8MB of RAM
print(f"List size: {len(million_numbers)} items")

# With iterators - generates on demand
million_range = range(1000000)  # Uses ~48 bytes!
print(f"Iterator created: {million_range}")

# Only creates values when needed
for i in million_range:
    if i > 5:
        break  # Stopped after 6 iterations, not a million!
Enter fullscreen mode Exit fullscreen mode

"Iterators," Margaret explained, "generate values lazily—only when requested. This is why range() in Python 3 doesn't create a massive list. It's an iterator that produces numbers on demand."

The Iterator Protocol

Margaret revealed the two special methods that made iteration work:

class SimpleIterator:
    def __init__(self, data):
        self.data = data
        self.index = 0

    def __iter__(self):
        # Return an iterator (for iterators, return self)
        return self

    def __next__(self):
        # Return the next value
        if self.index >= len(self.data):
            raise StopIteration  # Signal we're done

        value = self.data[self.index]
        self.index += 1
        return value

# Use it
iterator = SimpleIterator([1, 2, 3])
print(next(iterator))  # 1
print(next(iterator))  # 2
print(next(iterator))  # 3
print(next(iterator))  # Raises StopIteration
Enter fullscreen mode Exit fullscreen mode

"The __iter__ method," Margaret explained, "returns an iterator object. The __next__ method returns the next value, or raises StopIteration when exhausted."

The Protocol Summary

Margaret drew a diagram in her notebook:

THE ITERATOR PROTOCOL:

Iterable:
  - Has __iter__() method
  - Returns a new iterator each call
  - Examples: list, str, dict, custom collections

Iterator:
  - Has __iter__() method (returns self)
  - Has __next__() method (returns next value or raises StopIteration)
  - Single-use: exhausted after one iteration
  - Examples: list_iterator, generator, file object
Enter fullscreen mode Exit fullscreen mode

"Every iterator is iterable," she noted, "but not every iterable is an iterator."

Why Iterators Return Self

Timothy asked why __iter__ returned self for iterators.

"Because iterators are already positioned in the sequence," Margaret explained. "They're stateful—they remember where they are. Returning self says 'I'm already an iterator, use me as-is.'"

iterator = iter([1, 2, 3])

# Calling iter() on an iterator returns the same object
same_iterator = iter(iterator)
print(iterator is same_iterator)  # True - same object!

# This allows iterators to work in for loops
for value in iterator:  # for loop calls iter(iterator), which returns itself
    print(value)
Enter fullscreen mode Exit fullscreen mode

"In contrast," she continued, "iterables return new iterators each time to allow independent iteration."

Iterables That Aren't Iterators

Timothy discovered that many iterables produce new iterators each time:

class BookCollection:
    def __init__(self, books):
        self.books = books

    def __iter__(self):
        # Return a NEW iterator each time
        return BookCollectionIterator(self.books)

class BookCollectionIterator:
    def __init__(self, books):
        self.books = books
        self.index = 0

    def __iter__(self):
        # Iterators return themselves
        return self

    def __next__(self):
        if self.index >= len(self.books):
            raise StopIteration
        book = self.books[self.index]
        self.index += 1
        return book

# This pattern allows multiple iterations
collection = BookCollection(["1984", "Dune"])

# First iteration
for book in collection:
    print(book)

# Second iteration - works because we get a new iterator
for book in collection:
    print(book)
Enter fullscreen mode Exit fullscreen mode

"Separating the iterable from the iterator," Margaret noted, "allows iterating multiple times. Each for loop gets a fresh iterator with its own state."

How Generators Are Iterators

Timothy had an epiphany connecting to his generator knowledge:

def book_generator():
    yield "1984"
    yield "Dune"
    yield "Foundation"

gen = book_generator()

# Generators implement the iterator protocol!
print(hasattr(gen, '__iter__'))  # True
print(hasattr(gen, '__next__'))  # True

print(next(gen))  # 1984
print(next(gen))  # Dune
Enter fullscreen mode Exit fullscreen mode

"Generators," Margaret explained, "are iterators created with yield. Python automatically implements __iter__ and __next__ for you. That's why they're so convenient—you get the iterator protocol without writing the boilerplate."

The iter() Built-in Function

Margaret showed Timothy two forms of the iter() function:

# Form 1: Get iterator from iterable
books = ["1984", "Dune"]
iterator = iter(books)

# Form 2: Create iterator from callable and sentinel
with open('catalog.txt') as f:
    # Read until empty string (sentinel)
    for line in iter(f.readline, ''):
        if line.strip():
            print(line)
Enter fullscreen mode Exit fullscreen mode

The second form was less common but powerful: it repeatedly called the function until it returned the sentinel value.

Built-in Iterators

Timothy explored Python's built-in iterators:

# range - generates numbers on demand
for i in range(1000000):  # Doesn't create a million-item list!
    if i > 5:
        break

# enumerate - adds index to iteration
books = ["1984", "Dune", "Foundation"]
for index, book in enumerate(books):
    print(f"{index}: {book}")

# zip - iterates multiple sequences in parallel
titles = ["1984", "Dune"]
authors = ["Orwell", "Herbert"]
for title, author in zip(titles, authors):
    print(f"{title} by {author}")

# reversed - iterates in reverse (requires sequence with __len__ and __getitem__)
for book in reversed(books):
    print(book)
Enter fullscreen mode Exit fullscreen mode

Each of these created iterators that produced values on demand, rather than building complete lists in memory.

Manual Iteration with next()

Timothy learned he could manually control iteration:

books = iter(["1984", "Dune", "Foundation"])

# Process items manually
first = next(books)
print(f"First: {first}")

second = next(books)
print(f"Second: {second}")

# Process remaining with for loop
for book in books:  # Continues from where we left off
    print(f"Remaining: {book}")
Enter fullscreen mode Exit fullscreen mode

The iterator maintained state, so the for loop picked up where next() left off.

The Default Value Pattern

Margaret showed Timothy how to provide a default when an iterator exhausted:

books = iter(["1984"])

print(next(books))           # 1984
print(next(books, "No more"))  # No more (instead of StopIteration)
Enter fullscreen mode Exit fullscreen mode

The second argument to next() became the default return value when StopIteration would otherwise be raised.

Iterator Exhaustion

Timothy encountered a critical behavior:

books = iter(["1984", "Dune", "Foundation"])

# First iteration
list1 = list(books)
print(list1)  # ['1984', 'Dune', 'Foundation']

# Second iteration - empty!
list2 = list(books)
print(list2)  # []
Enter fullscreen mode Exit fullscreen mode

"Iterators," Margaret warned, "are single-use. Once exhausted, they're done. If you need multiple iterations, either get a new iterator from the iterable, or convert to a list first."

# Solution 1: Keep the iterable, create new iterators
book_list = ["1984", "Dune", "Foundation"]
for book in book_list:  # New iterator each time
    print(book)
for book in book_list:  # Another new iterator
    print(book)

# Solution 2: Convert iterator to list
book_iterator = iter(["1984", "Dune", "Foundation"])
books = list(book_iterator)  # Now we can iterate multiple times
Enter fullscreen mode Exit fullscreen mode

The Iterator Chain

Timothy discovered he could chain iterators:

import itertools

classics = ["1984", "Brave New World"]
scifi = ["Dune", "Foundation"]

# Chain iterators together
for book in itertools.chain(classics, scifi):
    print(book)
# 1984
# Brave New World  
# Dune
# Foundation
Enter fullscreen mode Exit fullscreen mode

The itertools module provided tools for working with iterators efficiently.

Infinite Iterators

Margaret demonstrated iterators that never exhausted:

import itertools

# Count forever
counter = itertools.count(start=1, step=1)
for i in counter:
    print(i)
    if i >= 5:
        break

# Cycle through values infinitely
colors = itertools.cycle(['red', 'green', 'blue'])
for i, color in enumerate(colors):
    print(color)
    if i >= 7:
        break
# red, green, blue, red, green, blue, red, green
Enter fullscreen mode Exit fullscreen mode

Infinite iterators were safe because they produced values lazily—they only generated what was actually consumed.

The iter() Sentinel Pattern in Practice

Timothy saw a practical use of the sentinel form:

# Read file in chunks until empty
with open('large_catalog.txt', 'rb') as f:
    # The lambda creates a no-argument callable that iter() calls repeatedly
    # Each call executes f.read(4096), stopping when it returns b'' (the sentinel)
    for chunk in iter(lambda: f.read(4096), b''):
        process_chunk(chunk)

# Keep asking for input until user says "quit"
for user_input in iter(lambda: input("Enter book title (or 'quit'): "), "quit"):
    add_book_to_catalog(user_input)
Enter fullscreen mode Exit fullscreen mode

"The lambda," Margaret explained, "creates a no-argument callable that iter() can invoke repeatedly. Each call executes the function body, and when it returns the sentinel value, iteration stops. This pattern is perfect for reading until a condition is met."

Checking if Something is Iterable

Timothy learned to test for iteration support:

from collections.abc import Iterable, Iterator

books = ["1984", "Dune"]
book_iter = iter(books)

print(isinstance(books, Iterable))      # True
print(isinstance(books, Iterator))      # False - iterables aren't iterators

print(isinstance(book_iter, Iterable))  # True - iterators are iterable
print(isinstance(book_iter, Iterator))  # True
Enter fullscreen mode Exit fullscreen mode

"All iterators are iterable," Margaret explained, "because they implement __iter__ (which returns themselves). But not all iterables are iterators—lists, for example, produce new iterators each time."

Timothy's Iterator Protocol Wisdom

Through mastering the Conveyor Belt Protocol, Timothy learned essential principles:

For loops use the iterator protocol: Every for loop calls iter() then repeatedly calls next().

Iterables have iter: They produce iterators when iter() is called.

Iterators have next: They produce the next value or raise StopIteration.

Iterators return self from iter: Because they're already positioned and stateful.

Iterables return new iterators: To allow independent, multiple iterations.

StopIteration signals exhaustion: This exception tells Python there are no more values.

Iterators are single-use: Once exhausted, they're done. Get a new iterator to iterate again.

Memory efficiency is the superpower: Iterators generate values on demand, not all upfront.

Generators are iterators: They implement the iterator protocol automatically.

Separate iterable from iterator: For multiple iterations, make the iterable produce new iterators.

Built-ins use iterators: range, enumerate, zip, and reversed all return iterators.

Manual control with next(): You can step through iterators yourself, mixing with for loops.

Check with isinstance(): Use collections.abc.Iterable and Iterator to test.

Timothy's exploration of the Iterator Protocol revealed the machinery beneath Python's most common construct—the for loop. The brass gears and leather belts of the conveyor system, once mysterious, now made perfect sense. Understanding the protocol meant he could create his own iterables, debug iteration problems, and appreciate how Python's lazy evaluation worked at the foundational level. The conveyor belts would deliver books one at a time, efficiently and elegantly, following a protocol centuries old yet perfectly modern.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)