Understanding Python's Iteration Protocol: Iterable and Iterator with Practical Examples
This article explains Python's iteration protocol, detailing how iterable objects and iterators work, and provides multiple code examples—including custom iterables, generators, and iterator-based test data handling—to demonstrate their use in building flexible and memory‑efficient applications.
The iteration protocol in Python defines how objects can be traversed using constructs like for loops and built‑in functions such as list() and tuple() . An object is iterable if it implements __iter__() , which must return an iterator; an iterator implements __next__() and raises StopIteration when exhausted.
1. Iterable Example
class MyIterable:
def __init__(self, data):
self.data = data
def __iter__(self):
return MyIterator(self.data)
class MyIterator:
def __init__(self, data):
self.data = data
self.position = 0
def __next__(self):
if self.position >= len(self.data):
raise StopIteration
item = self.data[self.position]
self.position += 1
return item
my_iterable = MyIterable([1, 2, 3])
for item in my_iterable:
print(item) # Output: 1 2 32. Iterator Example
class MyIterator:
def __init__(self, data):
self.data = data
self.position = 0
def __next__(self):
if self.position >= len(self.data):
raise StopIteration
item = self.data[self.position]
self.position += 1
return item
my_iterator = MyIterator([1, 2, 3])
print(next(my_iterator)) # 1
print(next(my_iterator)) # 2
print(next(my_iterator)) # 3Comprehensive Custom List Example
class CustomList:
def __init__(self, initial_data=None):
self.data = [] if initial_data is None else list(initial_data)
def add(self, item):
self.data.append(item)
def __iter__(self):
return CustomIterator(self.data)
class CustomIterator:
def __init__(self, data):
self.data = data
self.position = 0
def __next__(self):
if self.position >= len(self.data):
raise StopIteration
item = self.data[self.position]
self.position += 1
return item
custom_list = CustomList([1, 2, 3])
custom_list.add(4)
custom_list.add(5)
for item in custom_list:
print(item) # Output: 1 2 3 4 5
print(list(custom_list)) # [1, 2, 3, 4, 5]
print(tuple(custom_list)) # (1, 2, 3, 4, 5)
print(sum(custom_list)) # 15
print(sorted(custom_list))# [1, 2, 3, 4, 5]Advanced Use Cases
Example 1 shows a generator that yields test cases from a YAML file, allowing memory‑efficient processing of large datasets.
import requests, yaml
def generate_test_cases(filename):
with open(filename, 'r') as stream:
test_cases = yaml.safe_load(stream)['tests']
for test_case in test_cases:
yield test_case
def test_api_with_generator():
generator = generate_test_cases('data/api_tests.yaml')
for test_case in generator:
response = requests.request(test_case['method'], test_case['url'], headers=test_case.get('headers', {}), json=test_case.get('body', {}))
assert response.status_code == test_case['expected_status']
test_api_with_generator()Example 2 implements a dynamic test‑case iterator that constructs test dictionaries on the fly.
class DynamicTestCases:
def __init__(self, urls, methods, headers, bodies, expected_statuses):
self.urls = urls
self.methods = methods
self.headers = headers
self.bodies = bodies
self.expected_statuses = expected_statuses
self.counter = 0
def __iter__(self):
return self
def __next__(self):
if self.counter < len(self.urls):
test_case = {
'url': self.urls[self.counter],
'method': self.methods[self.counter],
'headers': self.headers[self.counter],
'body': self.bodies[self.counter],
'expected_status': self.expected_statuses[self.counter]
}
self.counter += 1
return test_case
else:
raise StopIteration
def test_api_with_iterator():
urls = ["https://api.example.com/user/123", "https://api.example.com/user/456"]
methods = ["GET", "POST"]
headers = [{"Authorization": "Bearer token1"}, {"Authorization": "Bearer token2"}]
bodies = [{}, {"name": "Jane Doe", "email": "[email protected]"}]
expected_statuses = [200, 201]
test_cases = DynamicTestCases(urls, methods, headers, bodies, expected_statuses)
for tc in test_cases:
resp = requests.request(tc['method'], tc['url'], headers=tc['headers'], json=tc['body'])
assert resp.status_code == tc['expected_status']
test_api_with_iterator()Example 3 demonstrates a page iterator for paginated API responses.
class PageIterator:
def __init__(self, base_url, params):
self.base_url = base_url
self.params = params
self.page = 1
def __iter__(self):
return self
def __next__(self):
response = requests.get(self.base_url, params={**self.params, "page": self.page})
if response.status_code != 200:
raise RuntimeError(f"Request failed with status code {response.status_code}")
data = response.json()
if not data['results']:
raise StopIteration
self.page += 1
return data['results']
def test_pagination():
iterator = PageIterator("https://api.example.com/data", {"limit": 10})
for page in iterator:
print(page)
test_pagination()Example 4 uses an iterator to iterate over multiple test environments loaded from a YAML file.
class EnvironmentIterator:
def __init__(self, filename):
with open(filename, 'r') as stream:
self.environments = yaml.safe_load(stream)['environments']
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index < len(self.environments):
env = self.environments[self.index]
self.index += 1
return env
else:
raise StopIteration
def test_environments():
env_iter = EnvironmentIterator('data/environments.yaml')
for env in env_iter:
resp = requests.get(f"{env['base_url']}/health")
assert resp.status_code == 200
test_environments()Example 5 shows a generator expression that simplifies test case definition.
def test_api_with_generator_expression():
test_cases = (
{'url': "https://api.example.com/user/123", 'method': "GET", 'headers': {"Authorization": "Bearer token1"}, 'expected_status': 200},
{'url': "https://api.example.com/user", 'method': "POST", 'headers': {"Authorization": "Bearer token2"}, 'body': {"name": "Jane Doe", 'email': "[email protected]"}, 'expected_status': 201}
)
for tc in test_cases:
resp = requests.request(tc['method'], tc['url'], headers=tc.get('headers'), json=tc.get('body'))
assert resp.status_code == tc['expected_status']
test_api_with_generator_expression()In summary, mastering the iteration protocol enables developers to create custom data structures that integrate seamlessly with Python's control flow and built‑in utilities, leading to more flexible, readable, and maintainable code, especially in automated testing scenarios.
Test Development Learning Exchange
Test Development Learning Exchange
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.