Introduction
Welcome back. In our last lesson, you learned how to give your custom objects a voice by implementing __str__ for user-friendly output and __repr__ for clear, developer-focused representation. You've made your objects inspectable. Now, let's make them explorable.
This lesson dives into the iteration protocol. It's the mechanism that powers Python's for loops, list comprehensions, and many other language features. Our learning outcome is to implement the iteration protocol (__iter__/__next__) correctly, avoiding the common mistake of returning self from __iter__ in a container.
By the end of this lesson, you'll understand the critical distinction between an iterable and an iterator, and you'll be able to implement the protocol in a way that makes your custom objects behave just like Python's built-in collections, complete with safe, independent iteration.
The Iteration Protocol: Iterable vs. Iterator
At its heart, Python's iteration is a simple contract between two types of objects:
- Iterable: An object that can be looped over. It signals this ability by having an
__iter__dunder method. Its one job is to produce an iterator. Examples include lists, tuples, and strings. - Iterator: An object that manages the state of an iteration. It produces the next value with a
__next__method and signals the end by raising aStopIterationexception. It also has an__iter__method that simply returnsself.

When you write a for loop, Python performs these steps under the hood:
- It calls the built-in
iter()function on the iterable object (e.g., your list). - The
iter()function calls the object's__iter__method, which returns an iterator object. - The
forloop then repeatedly calls thenext()function on this iterator object to get each item. - When the iterator runs out of items, its
__next__method raisesStopIteration, and the loop terminates.
This first video provides a foundational explanation of this distinction.
Python Tutorial: Iterators and Iterables - What Are They and How Do They Work?
Let's start with a clear definition of iterables and iterators. This video from Corey Schafer explains the core concepts using a simple list as an example.
Watch from the beginning to 04:39. Focus on how a list is defined as an iterable (it has __iter__) but not an iterator (it lacks __next__).
The Pitfall: Shared Iteration State
Now for the critical part of our learning outcome. Why is it a common mistake for an iterable container's __iter__ method to return self? Because it creates shared state. If __iter__ returns the same iterator instance every time, you can't have multiple, independent loops over the same object.
Consider how a list behaves. You expect to be able to do this:
my_list = ['a', 'b', 'c']
# Get two independent iterators from the same list
iter1 = iter(my_list)
iter2 = iter(my_list)
print(f"iter1 is iter2? {iter1 is iter2}") # False! They are different objects.
# Advancing one does not affect the other
print(f"Next from iter1: {next(iter1)}") # 'a'
print(f"Next from iter2: {next(iter2)}") # 'a'
print(f"Next from iter1: {next(iter1)}") # 'b'
If list.__iter__ simply returned self, both iter1 and iter2 would point to the same object. Advancing iter1 would also advance iter2, leading to confusing bugs.
This article section explains this concept perfectly.
Mastering Python's Iteration Protocol
This article from Dev.to explains why an iterable should return a brand new, independent iterator instance each time iter() is called.
Read the section titled 'Aside: The Importance of Brand New Independent Instances'. The code example demonstrates exactly why independent iterators are essential for predictable behavior.
The Correct Pattern: Separating the Iterable from the Iterator
To avoid shared state, the standard pattern is to use two classes:
- The main container class (the iterable), which holds the data.
- A private helper iterator class, which manages the state for a single iteration.
Let's design a WordList class that holds a sentence and allows iteration over its words.

Following this pattern, our WordList implementation would look like this:
# The Iterator class
class _WordListIterator:
def __init__(self, words):
self._words = words
self._index = 0
def __next__(self):
if self._index >= len(self._words):
raise StopIteration
# Get the word and advance the index for the next call
word = self._words[self._index]
self._index += 1
return word
def __iter__(self):
# An iterator's __iter__ should always return itself
return self
# The Iterable class
class WordList:
def __init__(self, text):
self._words = text.split()
def __iter__(self):
# The key: always return a NEW, fresh iterator instance
return _WordListIterator(self._words)
# --- Usage ---
sentence = WordList("Python's data model is powerful")
# The `for` loop automatically calls __iter__ to get a new iterator
for word in sentence:
print(word)
# You can create multiple independent iterators
iter_a = iter(sentence)
iter_b = iter(sentence)
print(next(iter_a)) # Prints 'Python's'
print(next(iter_b)) # Also prints 'Python's'
print(next(iter_a)) # Prints 'data'
This separation guarantees that each for loop or call to iter(sentence) gets a clean start, with no risk of interference.
When Is It Okay for __iter__ to Return self?
So, is it always wrong for __iter__ to return self? No. It's perfectly correct for an object that is designed to be its own iterator.
These are typically objects that represent a single, non-resettable stream of data, not a reusable container. Think of a file handle or a network socket. You read from it once. You don't expect to create multiple independent "readers" from the same file handle object.
This video demonstrates creating a MyRange class that acts as its own iterator. This is a valid design because a range object is often used for a single pass in a loop.
Python Tutorial: Iterators and Iterables - What Are They and How Do They Work?
Let's look at a case where an object is its own iterator. This will clarify the distinction between a reusable container and a single-pass iterator object.
Watch from 10:09 to 15:08. Notice how the MyRange class has both __iter__ and __next__ methods, and __iter__ correctly returns self. This object is both an iterable and an iterator.
The rule of thumb is:
- If your object is a reusable container (like a list, set, or our
WordList), its__iter__method must return a new iterator instance. - If your object is an iterator itself (representing a single stream), its
__iter__method should returnself.
Exercise: Make a Sentence Class Iterable
Your task is to properly implement the iteration protocol for the Sentence class below. You must use the two-class pattern (a Sentence iterable and a _SentenceIterator iterator) to ensure that multiple iterations are independent.
# sentence.py
class _SentenceIterator:
"""The stateful iterator for the Sentence class."""
def __init__(self, words):
# TODO: Initialize state here
pass
def __next__(self):
# TODO: Return the next word or raise StopIteration
pass
def __iter__(self):
# TODO: An iterator is its own iterable
pass
class Sentence:
"""A container class that holds a sentence and is iterable."""
def __init__(self, text: str):
if not isinstance(text, str):
raise TypeError("Sentence must be initialized with a string.")
self.words = text.split()
def __iter__(self):
# TODO: Return a new, independent iterator object
pass
# --- Tests to verify your implementation ---
my_sentence = Sentence("the quick brown fox")
# Test 1: Basic iteration
words = [word for word in my_sentence]
assert words == ["the", "quick", "brown", "fox"]
# Test 2: Ensure StopIteration is raised correctly
it = iter(my_sentence)
assert next(it) == "the"
assert next(it) == "quick"
assert next(it) == "brown"
assert next(it) == "fox"
try:
next(it)
# This line should not be reached
assert False, "StopIteration was not raised"
except StopIteration:
# This is the expected outcome
pass
# Test 3: The most important test! Ensure iterators are independent.
iter1 = iter(my_sentence)
iter2 = iter(my_sentence)
assert iter1 is not iter2, "iter() should return a new iterator each time"
assert next(iter1) == "the"
assert next(iter1) == "quick"
assert next(iter2) == "the", "Iterator 2 should be independent of Iterator 1"
print("All tests passed!")
Click to see a possible solution
# sentence.py (solution)
class _SentenceIterator:
"""The stateful iterator for the Sentence class."""
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
if self.index >= len(self.words):
raise StopIteration
current_word = self.words[self.index]
self.index += 1
return current_word
def __iter__(self):
return self
class Sentence:
"""A container class that holds a sentence and is iterable."""
def __init__(self, text: str):
if not isinstance(text, str):
raise TypeError("Sentence must be initialized with a string.")
self.words = text.split()
def __iter__(self):
# The key to correctness: return a NEW instance of the iterator.
return _SentenceIterator(self.words)
Conclusion
You have now implemented one of the most fundamental protocols in Python. By correctly separating the responsibilities of the iterable and the iterator, you can create custom container objects that are robust, predictable, and integrate seamlessly with the rest of the language.
Key Takeaways:
- An iterable has
__iter__and its job is to produce an iterator. - An iterator has
__next__to provide values and__iter__to return itself. It is stateful. - The
forloop uses this protocol by first getting an iterator, then repeatedly callingnext()on it. - For reusable containers,
__iter__must return a new, independent iterator instance to prevent bugs from shared state. - Objects that are inherently single-pass streams (like file handles) can be their own iterators, in which case
__iter__correctly returnsself.
Writing a separate iterator class can feel verbose. As you've seen in some of the resources, Python offers a much more elegant and concise way to create iterators.
In our next lesson, we'll explore generators and the yield keyword. You'll learn how to write a single __iter__ method that does the work of an entire iterator class in just a few lines of code.