Skip to content

Behavioral Patterns: Iterator, Template Method, Visitor

Iterator

Idea: Walk through a collection step by step without knowing its inner layout.

Python uses the iterator protocol: __iter__ and __next__, or generators with yield.

from collections.abc import Iterator


class NumberRange:
    def __init__(self, start: int, end: int, step: int = 1) -> None:
        self._start = start
        self._end = end
        self._step = step

    def __iter__(self) -> Iterator[int]:
        current = self._start
        while current < self._end:
            yield current
            current += self._step


for n in NumberRange(0, 10, 2):
    print(n)  # 0, 2, 4, 6, 8

Custom iterators help when data is lazy: paginated APIs, DB cursors, or large files you do not want to load fully.

Where it helps: Cursor pagination, API page loops, streaming files, graph walks (BFS/DFS as generators).

Template Method

Idea: The base class fixes the order of steps. Subclasses only fill in the steps. The skeleton stays stable.

from abc import ABC, abstractmethod


class DataImporter(ABC):
    def run(self) -> None:
        """Template: fixed sequence of steps."""
        raw = self.load_data()
        validated = self.validate(raw)
        self.save(validated)

    @abstractmethod
    def load_data(self) -> list[dict]: ...

    @abstractmethod
    def validate(self, data: list[dict]) -> list[dict]: ...

    def save(self, data: list[dict]) -> None:
        print(f"Saving {len(data)} records")


class CSVImporter(DataImporter):
    def load_data(self) -> list[dict]:
        return [{"name": "Alice"}]  # parse CSV

    def validate(self, data: list[dict]) -> list[dict]:
        return [r for r in data if r.get("name")]


class JSONImporter(DataImporter):
    def load_data(self) -> list[dict]:
        return [{"name": "Bob"}]  # parse JSON

    def validate(self, data: list[dict]) -> list[dict]:
        return data

Where it helps: Import pipelines, report jobs (load → process → format → export), batch frameworks.

Visitor

Idea: Add a new operation over a tree of types by writing a visitor. You often avoid changing each node class for every new operation.

from abc import ABC, abstractmethod
from dataclasses import dataclass


class ASTNode(ABC):
    @abstractmethod
    def accept(self, visitor: "ASTVisitor") -> None: ...


class ASTVisitor(ABC):
    @abstractmethod
    def visit_number(self, node: "NumberNode") -> None: ...

    @abstractmethod
    def visit_add(self, node: "AddNode") -> None: ...


@dataclass
class NumberNode(ASTNode):
    value: int

    def accept(self, visitor: ASTVisitor) -> None:
        visitor.visit_number(self)


@dataclass
class AddNode(ASTNode):
    left: ASTNode
    right: ASTNode

    def accept(self, visitor: ASTVisitor) -> None:
        visitor.visit_add(self)


class PrintVisitor(ASTVisitor):
    def visit_number(self, node: NumberNode) -> None:
        print(node.value, end="")

    def visit_add(self, node: AddNode) -> None:
        node.left.accept(self)
        print("+", end="")
        node.right.accept(self)

Where it helps: Compiler AST passes, document export (HTML vs PDF visitor), pricing rules over a cart tree.

Behavioral pattern risks

Pattern Risk
Strategy Many tiny classes; simple cases may need only functions
Observer Leaks if not unsubscribed; unclear order; ripple updates
Command Undo and history can grow; complex operations are hard to reverse
Chain Request may leave the chain with no handler
State Too many states for simple flows; messy transitions
Mediator Mediator grows into a “god” module
Iterator Shared iterators need care with threads
Template Method Strong tie to inheritance; subclasses can break rules
Visitor New node types mean every visitor must change