Skip to content

Iterables and Containers

Make your objects work with `for` loops and the `in` operator.

Python allows you to make your objects iterable and work as containers, enabling them to work seamlessly with for loops, comprehensions, and membership testing.

An object is iterable if it implements __iter__(), which returns an iterator. The iterator must implement __next__() to return the next item.

date_range.py
from datetime import datetime, timedelta
class DateRange:
"""Custom iterable for date ranges"""
def __init__(self, start_date: datetime, end_date: datetime):
self.start_date = start_date
self.end_date = end_date
def __iter__(self):
"""Returns iterator object"""
self.current_date = self.start_date
return self
def __next__(self):
"""Returns next item in sequence"""
if self.current_date > self.end_date:
raise StopIteration
date_to_return = self.current_date
self.current_date += timedelta(days=1)
return date_to_return
# Usage
for date in DateRange(datetime(2023, 1, 1), datetime(2023, 1, 5)):
print(date.strftime("%Y-%m-%d"))
# Output:
# 2023-01-01
# 2023-01-02
# 2023-01-03
# 2023-01-04
# 2023-01-05
paginated_results.py
class PaginatedResults:
"""Iterable for paginated API responses"""
def __init__(self, fetch_page_func, total_pages: int):
self.fetch_page = fetch_page_func
self.total_pages = total_pages
def __iter__(self):
self.current_page = 1
return self
def __next__(self):
if self.current_page > self.total_pages:
raise StopIteration
page_data = self.fetch_page(self.current_page)
self.current_page += 1
return page_data
# Usage
def fetch_api_page(page_num):
return [f"Item {page_num}-{i}" for i in range(5)]
results = PaginatedResults(fetch_api_page, total_pages=3)
for page in results:
print(page)

Containers implement __contains__() for membership testing with the in keyword. They often also implement __len__() and __getitem__().

element in container

This translates to:

container.__contains__(element)
boundary_container.py
class Boundaries:
"""Container that checks if a point is within boundaries"""
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def __contains__(self, point: tuple) -> bool:
"""Check if point (x, y) is within boundaries"""
x, y = point
return 0 <= x <= self.width and 0 <= y <= self.height
class Grid:
"""Grid that uses Boundaries via composition"""
def __init__(self, width: float, height: float):
self.width = width
self.height = height
self.limits = Boundaries(width, height) # Composition
def __contains__(self, point: tuple) -> bool:
"""Delegate to Boundaries"""
return point in self.limits
# Usage
grid = Grid(10, 10)
print((5, 5) in grid) # True
print((15, 5) in grid) # False
print((5, 15) in grid) # False
classDiagram
    class Grid {
        -width: float
        -height: float
        -limits: Boundaries
        +__contains__(point: tuple) bool
    }
    
    class Boundaries {
        -width: float
        -height: float
        +__contains__(point: tuple) bool
    }
    
    Grid *-- Boundaries : composition

You can make objects both iterable and containers:

iterable_container.py
class NumberRange:
"""Range that is both iterable and a container"""
def __init__(self, start: int, end: int):
self.start = start
self.end = end
def __iter__(self):
"""Make it iterable"""
self.current = self.start
return self
def __next__(self):
"""Next value in iteration"""
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
def __contains__(self, value: int) -> bool:
"""Make it a container"""
return self.start <= value <= self.end
def __len__(self) -> int:
"""Length of the range"""
return max(0, self.end - self.start + 1)
# Usage
range_obj = NumberRange(1, 10)
# Iterable - works with for loops
for num in range_obj:
print(num, end=" ") # 1 2 3 4 5 6 7 8 9 10
# Container - works with 'in'
print(5 in range_obj) # True
print(15 in range_obj) # False
# Length
print(len(range_obj)) # 10
custom_collection.py
class TaggedItems:
"""Collection that supports iteration and membership testing"""
def __init__(self):
self.items = []
self.tags = {}
def add_item(self, item: str, tags: list[str]):
"""Add item with tags"""
self.items.append(item)
self.tags[item] = tags
def __iter__(self):
"""Iterate over items"""
return iter(self.items)
def __contains__(self, item: str) -> bool:
"""Check if item exists"""
return item in self.items
def __len__(self) -> int:
"""Number of items"""
return len(self.items)
def items_with_tag(self, tag: str):
"""Generator for items with specific tag"""
for item in self.items:
if tag in self.tags.get(item, []):
yield item
# Usage
collection = TaggedItems()
collection.add_item("Python", ["programming", "language"])
collection.add_item("JavaScript", ["programming", "web"])
collection.add_item("SQL", ["database", "query"])
# Iterable
for item in collection:
print(item)
# Container
print("Python" in collection) # True
print("Ruby" in collection) # False
# Length
print(len(collection)) # 3
# Custom iteration
for item in collection.items_with_tag("programming"):
print(item) # Python, JavaScript