Skip to content
Python Basics: Building Classes That Support Iteration

Python Basics: Building Classes That Support Iteration

October 20, 2026

A class can work in for loops by implementing the iteration protocol. Separate a one-pass iterator from a reusable iterable so callers know whether a second traversal starts over.

These three standalone examples were tested on Linux with Python 3.12.14. They require no third-party packages. Assertions check the results; run without Python’s -O option.

Prepare a scratch directory

You need Bash, Python 3.12 or newer on your PATH, a text editor, and the mktemp, rm, and rmdir utilities. Keep using the same terminal and save each named source file as UTF-8.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Implement a one-pass ticket iterator

Save as iterate_ticket_numbers.py:

class TicketIterator:
    def __init__(self, first, stop):
        self._current = first
        self._stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self._current >= self._stop:
            raise StopIteration
        ticket = self._current
        self._current += 1
        return ticket


tickets = TicketIterator(101, 104)
assert iter(tickets) is tickets
first = next(tickets)
remaining = list(tickets)
assert first == 101
assert remaining == [102, 103]
assert next(tickets, "done") == "done"
assert next(tickets, "done") == "done"
print("First:", first)
print("Remaining:", remaining)
print("Second traversal:", list(tickets))

Run it:

python3 iterate_ticket_numbers.py

Output:

First: 101
Remaining: [102, 103]
Second traversal: []

This class expects integer bounds and excludes stop. __next__ returns one ticket or raises StopIteration. __iter__ returns the same iterator without resetting its position. After exhaustion, further requests stay exhausted.

2. Return a fresh iterator for each traversal

Save as revisit_task_batch.py:

class TaskBatch:
    def __init__(self, tasks):
        self._tasks = tuple(tasks)

    def __iter__(self):
        return iter(self._tasks)


batch = TaskBatch(["lint", "test"])
left = iter(batch)
right = iter(batch)
assert left is not right
assert next(left) == "lint"
assert next(right) == "lint"
print("Left remainder:", list(left))
print("Right remainder:", list(right))
print("Full batch:", list(batch))
assert list(batch) == ["lint", "test"]
assert list(batch) == ["lint", "test"]

Run it:

python3 revisit_task_batch.py

Output:

Left remainder: ['test']
Right remainder: ['test']
Full batch: ['lint', 'test']

TaskBatch is reusable because each call to __iter__ returns a new tuple iterator. Two callers can advance independently. Construction stores a finite input as a tuple, so this design trades memory for repeatable traversal.

3. Wrap an input iterator with a filter

Save as iterate_nonblank_lines.py:

class NonblankLines:
    def __init__(self, lines):
        self._lines = iter(lines)

    def __iter__(self):
        return self

    def __next__(self):
        while True:
            line = next(self._lines).strip()
            if line:
                return line


lines = NonblankLines(["", "  build  ", "  ", "test"])
first = next(lines)
remaining = list(lines)
assert first == "build"
assert remaining == ["test"]
assert next(lines, "finished") == "finished"
print("First:", first)
print("Remaining:", remaining)
print("End:", next(lines, "finished"))

Run it:

python3 iterate_nonblank_lines.py

Output:

First: build
Remaining: ['test']
End: finished

This adapter expects strings. Each request may consume several blank inputs before returning a nonblank value. StopIteration from the wrapped iterator propagates naturally. A blank string is skipped; it is not confused with the end of the input.

When to use this

A generator is often shorter for a simple filter. An iterator class makes explicit state and methods available when a larger design needs them. Never reset an iterator inside __iter__; provide a separate reusable iterable when restarting is intended.

References

Clean up

In the same terminal, remove only these example files, then return to your original directory and remove the empty scratch directory:

rm -- iterate_ticket_numbers.py revisit_task_batch.py iterate_nonblank_lines.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on