By default, custom Python classes are not iterable. If you try to run a for loop over an instance of a class you defined, Python will raise a TypeError unless you explicitly implement the Iterator Protocol.
This guide explains how to make your objects compatible with loops, list comprehensions, and unpacking by implementing the __iter__ and __next__ magic methods, or by using the simpler generator approach.
Understanding the Iterator Protocol
To make an object iterable, Python expects it to follow a specific contract consisting of two special methods:
__iter__(self): This method initializes the iteration. It must return an iterator object (an object that implements__next__).__next__(self): This method returns the next value in the sequence. When there are no more items, it must raise theStopIterationexception to signal the loop to end.
If you try to iterate over a class without these, Python throws an error.
class Team:
def __init__(self, members):
self.members = members
my_team = Team(["Alice", "Bob", "Charlie"])
try:
for member in my_team:
print(member)
except TypeError as e:
print(f"Error: {e}")Output:
Error: 'Team' object is not iterableMethod 1: Implementing __iter__ and __next__
This is the explicit way to create an iterator. You define how the state changes with every step and explicitly signal when to stop.
In this example, we create a Countdown class that counts from a starting number down to zero.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 0:
raise StopIteration
num = self.current
self.current -= 1
return num
timer = Countdown(3)
for sec in timer:
print(sec)Output:
3
2
1
0Method 2: Using Generators (The Pythonic Way)
Implementing __next__ and managing internal state manually can be verbose. A more modern and readable approach is to turn __iter__ into a generator using the yield keyword.
Python automatically creates __next__ and handles the StopIteration logic for you when you use yield.
class Team:
def __init__(self, members):
self.members = members
def __iter__(self):
for member in self.members:
yield member
my_team = Team(["Alice", "Bob", "Charlie"])
print("--- Loop 1 ---")
for member in my_team:
print(member)
print("--- Loop 2 ---")
print(list(my_team))Output:
--- Loop 1 ---
Alice
Bob
Charlie
--- Loop 2 ---
['Alice', 'Bob', 'Charlie']Method 3: The Sequence Protocol (__getitem__)
Before the modern iterator protocol existed, Python used the Sequence Protocol. If __iter__ is missing, Python checks for __getitem__.
Python will try to access the object with index 0, then 1, then 2, and so on, until an IndexError is raised.
class Inventory:
def __init__(self):
self.items = ["Sword", "Shield", "Potion"]
def __getitem__(self, index):
return self.items[index]
bag = Inventory()
for item in bag:
print(f"Item: {item}")Output:
Item: Sword
Item: Shield
Item: PotionConclusion
To enable iteration on your custom classes:
- Use Generator Syntax (
__iter__+yield): This is the most efficient and readable method for 90% of use cases. - Use Iterator Protocol (
__iter__+__next__): Use this if you need fine-grained control over the internal state or are building a low-level custom iterator type. - Use Sequence Protocol (
__getitem__): Use this if your object naturally behaves like an ordered list or array that supports indexing.