Fundamentals 11 min read

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.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
Understanding Python's Iteration Protocol: Iterable and Iterator with Practical Examples

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 3

2. 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))  # 3

Comprehensive 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.

testingIteratorGeneratorIterableiteration-protocol
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.