Behavioral Pattern

Iterator

Provides a way to access elements of a collection sequentially.

Iterator Pattern: A Complete Guide with Examples in 11 Programming Languages

The Iterator Pattern is a behavioral design pattern that provides a standardized way to sequentially access elements of a collection without exposing the collection's underlying representation. Whether the data lives in an array, a linked list, a tree, a database cursor, a file on disk, or a lazily generated infinite sequence, the consumer always interacts with the same simple interface: ask for the next element, check if there are more. This uniform traversal contract is so fundamental that virtually every modern language has it baked directly into its core syntax — Python's for loop, JavaScript's for...of, Java's enhanced for, Rust's for loop, and Swift's for-in are all iterator protocol implementations under the hood.

In this comprehensive guide, we'll explore the Iterator Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.

Table of Contents

What is the Iterator Pattern?

The Iterator Pattern separates the concern of how to traverse a collection from what to do with each element. A collection (the Aggregate) creates an Iterator object that remembers the current position within the traversal. The client repeatedly calls next() on the iterator to advance through elements, and has_next() to check if the traversal is complete.

Think of it like a reading bookmark. The book (the collection) doesn't know how fast you read or where you are. The bookmark (the iterator) tracks your exact position. You can have multiple bookmarks in the same book — each remembers an independent position. You can even have bookmarks that traverse the book in different orders: front to back, back to front, or jumping chapter by chapter.

Why Use the Iterator Pattern?

The Iterator Pattern offers several compelling benefits:

  1. Single Responsibility: Traversal logic lives in the iterator, not the collection — collections focus on storage, iterators focus on traversal
  2. Multiple Simultaneous Traversals: Multiple independent iterators can traverse the same collection at different positions concurrently without interfering with each other
  3. Uniform Interface: Clients use the same interface regardless of whether the backing collection is an array, tree, graph, file, network stream, or generated sequence
  4. Lazy Evaluation: Iterators can generate or fetch elements on demand — processing infinite sequences or large datasets without loading everything into memory
  5. Alternative Traversal Strategies: The same collection can have multiple iterator types (forward, reverse, filtered, depth-first, breadth-first) without changing the collection's interface
  6. Algorithm Decoupling: Sorting, searching, transforming, and aggregating algorithms written against the iterator interface work with any collection type
  7. Language Integration: Implementing the iterator protocol makes your custom collections work seamlessly with built-in for loops, spread operators, and stream APIs

Iterator Pattern Comparison

Let's compare the Iterator Pattern with related behavioral patterns:

PatternPurposeWho Controls TraversalState LocationUse Case
IteratorSequential access to elementsClient (pulls)Iterator objectTraversing collections, lazy sequences
CompositeTree structure with uniform interfaceClient walks treeTree structureFile systems, UI hierarchies
VisitorOperations on object structureVisitor (pushes)Visitor objectAST transforms, reporting
GeneratorLazily produce a sequenceRuntime (coroutine)Coroutine frameInfinite sequences, pipelines
ObserverNotify subscribers of eventsPublisher (pushes)Subscriber listEvent systems, reactive UI

Key Distinctions:

  • Iterator vs. for-loop over array: Both traverse linearly. Iterator abstracts what is being traversed — the same loop body works on arrays, trees, database cursors, and HTTP paginated APIs. A for-loop over an array is hardcoded to that structure.
  • Iterator vs. Generator/Coroutine: Generators ARE iterators produced by a special coroutine mechanism. An iterator is the interface; a generator is one way to implement it. All generators are iterators; not all iterators are generators.
  • Pull (Iterator) vs. Push (Observer): Iterator is pull-based — the client requests the next item. Observer is push-based — the publisher delivers items to subscribers. Reactive streams (RxJava, RxJS) bridge both worlds.

Iterator Pattern Explained

The Iterator Pattern typically involves:

Core Components:

  1. Iterator Interface: Declares the traversal operations — minimally next() (return next element and advance) and has_next() / done (check if more elements exist). Often also includes current() (peek at current without advancing) and reset().
  2. Concrete Iterator: Implements the Iterator interface for a specific collection. Maintains a reference to the collection and the current traversal position (index, pointer, cursor). Each instance is an independent traversal.
  3. Aggregate Interface (Iterable/Collection): Declares the factory method that creates an iterator — typically __iter__(), iterator(), [Symbol.iterator](), or makeIterator(). The collection doesn't do the traversal; it just manufactures the iterator.
  4. Concrete Aggregate: Implements the Iterable interface. Stores the elements. Returns a new iterator instance on each call to the factory method.
  5. Client: Uses only the Iterator interface to traverse. Never interacts with the concrete collection internals.

Iterator Variants:

  • External Iterator: Client controls traversal by calling next() explicitly — maximum control, more verbose code
  • Internal Iterator: Iterator controls traversal and calls a callback for each element (e.g., .forEach()) — simpler client code, less control
  • Forward Iterator: Traverses front to back (most common)
  • Reverse Iterator: Traverses back to front
  • Filtering Iterator: Skips elements that don't match a predicate without materializing a filtered copy
  • Transforming Iterator: Maps each element through a function lazily
  • Composite Iterator: Traverses multiple collections as if they were one
  • Lazy/Infinite Iterator: Generates elements on demand — may never terminate

Class Diagram

Here's the UML class diagram showing the Iterator structure:

Beginner-Friendly Example

Let's start with a classic example: a custom book collection with multiple iterator types — a standard forward iterator, a reverse iterator, and a filtered iterator that only yields books in a given genre. This demonstrates the core value of the pattern: the same for loop body works with all three traversal strategies.

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TypeVar, Generic, Optional, Iterator as TypingIterator, List
from dataclasses import dataclass

T = TypeVar("T")


@dataclass
class Book:
    title:  str
    author: str
    genre:  str
    pages:  int

    def __repr__(self) -> str:
        return f'"{self.title}" by {self.author} [{self.genre}, {self.pages}pp]'


# ── Iterator Interface ────────────────────────────────────────────
class BookIterator(ABC):
    @abstractmethod
    def __iter__(self) -> "BookIterator":
        pass

    @abstractmethod
    def __next__(self) -> Book:
        pass

    def has_next(self) -> bool:
        """Convenience method — not required by Python's protocol."""
        try:
            # Peek ahead without advancing (peek by trying and caching)
            return True
        except StopIteration:
            return False


# ── Concrete Iterators ────────────────────────────────────────────
class ForwardBookIterator(BookIterator):
    def __init__(self, books: List[Book]) -> None:
        self._books    = books
        self._position = 0

    def __iter__(self) -> "ForwardBookIterator":
        return self

    def __next__(self) -> Book:
        if self._position >= len(self._books):
            raise StopIteration
        book = self._books[self._position]
        self._position += 1
        return book


class ReverseBookIterator(BookIterator):
    def __init__(self, books: List[Book]) -> None:
        self._books    = books
        self._position = len(books) - 1

    def __iter__(self) -> "ReverseBookIterator":
        return self

    def __next__(self) -> Book:
        if self._position < 0:
            raise StopIteration
        book = self._books[self._position]
        self._position -= 1
        return book


class GenreFilterIterator(BookIterator):
    """Lazy filtering iterator — skips non-matching books without a copy."""
    def __init__(self, books: List[Book], genre: str) -> None:
        self._books    = books
        self._genre    = genre.lower()
        self._position = 0

    def __iter__(self) -> "GenreFilterIterator":
        return self

    def __next__(self) -> Book:
        while self._position < len(self._books):
            book = self._books[self._position]
            self._position += 1
            if book.genre.lower() == self._genre:
                return book
        raise StopIteration


class LongBookIterator(BookIterator):
    """Yields only books above a minimum page count."""
    def __init__(self, books: List[Book], min_pages: int) -> None:
        self._books     = books
        self._min_pages = min_pages
        self._position  = 0

    def __iter__(self) -> "LongBookIterator":
        return self

    def __next__(self) -> Book:
        while self._position < len(self._books):
            book = self._books[self._position]
            self._position += 1
            if book.pages >= self._min_pages:
                return book
        raise StopIteration


# ── Concrete Collection ───────────────────────────────────────────
class BookShelf:
    """The Aggregate — stores books and creates iterators."""

    def __init__(self) -> None:
        self._books: List[Book] = []

    def add(self, book: Book) -> "BookShelf":
        self._books.append(book)
        return self  # Fluent API

    def __len__(self) -> int:
        return len(self._books)

    # Factory methods — each returns a different iterator type
    def __iter__(self) -> ForwardBookIterator:
        """Default iteration: forward."""
        return ForwardBookIterator(self._books)

    def reversed_iter(self) -> ReverseBookIterator:
        return ReverseBookIterator(self._books)

    def genre_iter(self, genre: str) -> GenreFilterIterator:
        return GenreFilterIterator(self._books, genre)

    def long_books_iter(self, min_pages: int = 400) -> LongBookIterator:
        return LongBookIterator(self._books, min_pages)


# ── Client ────────────────────────────────────────────────────────
def print_books(label: str, iterator: BookIterator) -> None:
    print(f"\n{label}:")
    for book in iterator:
        print(f"  {book}")


if __name__ == "__main__":
    shelf = BookShelf()
    shelf.add(Book("The Hobbit",           "Tolkien",     "Fantasy",   310))
    shelf.add(Book("Dune",                 "Herbert",     "Sci-Fi",    688))
    shelf.add(Book("1984",                 "Orwell",      "Dystopia",  328))
    shelf.add(Book("Foundation",           "Asimov",      "Sci-Fi",    244))
    shelf.add(Book("The Name of the Wind", "Rothfuss",    "Fantasy",   662))
    shelf.add(Book("Brave New World",      "Huxley",      "Dystopia",  311))
    shelf.add(Book("Hyperion",             "Simmons",     "Sci-Fi",    482))

    print(f"Shelf has {len(shelf)} books.")

    print_books("All books (forward)",  iter(shelf))
    print_books("All books (reverse)",  shelf.reversed_iter())
    print_books("Sci-Fi only",          shelf.genre_iter("sci-fi"))
    print_books("Dystopia only",        shelf.genre_iter("dystopia"))
    print_books("Long books (400+ pp)", shelf.long_books_iter(400))

    # Multiple independent iterators on the same shelf
    print("\n--- Two independent iterators running simultaneously ---")
    it1 = iter(shelf)
    it2 = shelf.genre_iter("sci-fi")
    print(f"it1 next: {next(it1)}")
    print(f"it2 next: {next(it2)}")
    print(f"it1 next: {next(it1)}")
    print(f"it2 next: {next(it2)}")

Production-Ready Example

Now let's tackle a realistic scenario: a lazy paginated API iterator. When fetching large result sets from a REST API (users, orders, log entries), you don't want to load all pages into memory upfront. A paginated iterator fetches one page at a time, transparently to the consumer — the for loop just keeps asking for the next item and the iterator handles page boundaries, fetch-on-demand, and end-of-results detection.

🐍 Python (Production Example)

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional, List, Iterator, TypeVar, Generic, Callable, Dict, Any
import math
import time


T = TypeVar("T")


# ─── Simulated API ────────────────────────────────────────────────
@dataclass
class User:
    id:       int
    name:     str
    email:    str
    active:   bool
    plan:     str

    def __repr__(self) -> str:
        status = "✓" if self.active else "✗"
        return f"User(id={self.id:3}, {status} {self.plan:8} | {self.name} <{self.email}>)"


@dataclass
class PageResult(Generic[T]):
    items:       List[T]
    page:        int
    page_size:   int
    total_items: int

    @property
    def total_pages(self) -> int:
        return math.ceil(self.total_items / self.page_size)

    @property
    def has_next(self) -> bool:
        return self.page < self.total_pages


class FakeUserAPI:
    """Simulates a paginated REST API backed by an in-memory dataset."""

    def __init__(self) -> None:
        self._users = [
            User(i, f"User {i:03}", f"user{i:03}@example.com",
                 active=(i % 3 != 0),
                 plan=["free", "basic", "premium"][i % 3])
            for i in range(1, 51)  # 50 users total
        ]
        self._fetch_count = 0

    def get_page(self, page: int, page_size: int = 10,
                 filters: Optional[Dict[str, Any]] = None) -> PageResult[User]:
        """Simulates a network call with latency."""
        self._fetch_count += 1
        time.sleep(0.01)  # Simulated network latency

        data = self._users
        if filters:
            if "plan" in filters:
                data = [u for u in data if u.plan == filters["plan"]]
            if "active" in filters:
                data = [u for u in data if u.active == filters["active"]]

        start = (page - 1) * page_size
        end   = start + page_size
        page_data = data[start:end]

        return PageResult(
            items=page_data,
            page=page,
            page_size=page_size,
            total_items=len(data)
        )

    @property
    def total_fetches(self) -> int:
        return self._fetch_count


# ─── Paginated Iterator ────────────────────────────────────────────
class PaginatedIterator(Generic[T]):
    """
    Lazily iterates over all pages of a paginated API.
    Fetches the next page only when the current page is exhausted.
    Fully implements Python's iterator protocol.
    """

    def __init__(
        self,
        fetch_page: Callable[[int], PageResult[T]],
        start_page: int = 1,
    ) -> None:
        self._fetch_page    = fetch_page
        self._current_page  = start_page
        self._page_items:   List[T] = []
        self._item_pos      = 0
        self._exhausted     = False
        self._pages_fetched = 0
        self._items_yielded = 0

        # Pre-fetch the first page
        self._load_next_page()

    def _load_next_page(self) -> None:
        result = self._fetch_page(self._current_page)
        self._pages_fetched += 1
        self._page_items = result.items
        self._item_pos   = 0

        if not result.items or not result.has_next:
            # This is the last page — mark exhausted after current items are consumed
            self._last_page = True
        else:
            self._last_page = False

        self._current_page += 1

    def __iter__(self) -> "PaginatedIterator[T]":
        return self

    def __next__(self) -> T:
        # If we've consumed all items on the current page, try to load the next
        if self._item_pos >= len(self._page_items):
            if self._last_page or self._exhausted:
                self._exhausted = True
                raise StopIteration
            self._load_next_page()

        # If page loaded empty (shouldn't happen with a well-behaved API)
        if not self._page_items:
            self._exhausted = True
            raise StopIteration

        item = self._page_items[self._item_pos]
        self._item_pos      += 1
        self._items_yielded += 1
        return item

    @property
    def stats(self) -> Dict[str, int]:
        return {
            "pages_fetched":  self._pages_fetched,
            "items_yielded":  self._items_yielded,
        }


# ─── Iterable Wrapper ─────────────────────────────────────────────
class PaginatedCollection(Generic[T]):
    """
    Iterable wrapper so PaginatedIterator can be used in for-loops.
    Each iteration creates a fresh iterator starting from page 1.
    """

    def __init__(
        self,
        fetch_fn: Callable[[int], PageResult[T]],
    ) -> None:
        self._fetch_fn = fetch_fn
        self._last_iter: Optional[PaginatedIterator[T]] = None

    def __iter__(self) -> PaginatedIterator[T]:
        self._last_iter = PaginatedIterator(self._fetch_fn)
        return self._last_iter

    def take(self, n: int) -> List[T]:
        """Convenience method: fetch only the first N items."""
        result = []
        for item in self:
            result.append(item)
            if len(result) >= n:
                break
        return result

    @property
    def last_iterator_stats(self) -> Optional[Dict[str, int]]:
        return self._last_iter.stats if self._last_iter else None


# ─── Composite Iterator: Merge multiple paginated sources ─────────
class MergedIterator(Generic[T]):
    """
    Merges multiple iterators into a single sequential stream.
    Useful for combining results from multiple API endpoints.
    """

    def __init__(self, *iterables: "PaginatedCollection[T]") -> None:
        self._iterables = iterables
        self._current_iter: Optional[Iterator[T]] = None
        self._source_idx = 0
        self._advance_source()

    def _advance_source(self) -> None:
        if self._source_idx < len(self._iterables):
            self._current_iter = iter(self._iterables[self._source_idx])
            self._source_idx += 1
        else:
            self._current_iter = None

    def __iter__(self) -> "MergedIterator[T]":
        return self

    def __next__(self) -> T:
        while self._current_iter is not None:
            try:
                return next(self._current_iter)
            except StopIteration:
                self._advance_source()
        raise StopIteration


# ─── Client ────────────────────────────────────────────────────────
if __name__ == "__main__":
    api = FakeUserAPI()

    # ── Scenario 1: Iterate ALL users lazily (5 pages × 10 items) ──
    print("=" * 60)
    print("SCENARIO 1: All users — lazy pagination")
    print("=" * 60)

    all_users = PaginatedCollection(
        lambda page: api.get_page(page, page_size=10)
    )

    count = 0
    for user in all_users:
        count += 1
        if count <= 3 or count >= 49:
            print(f"  {user}")
        elif count == 4:
            print("  ...")

    stats = all_users.last_iterator_stats
    print(f"\n  Total users: {count}")
    print(f"  Pages fetched: {stats['pages_fetched']}")
    print(f"  API calls so far: {api.total_fetches}")

    # ── Scenario 2: Take only first 15 items (only 2 pages fetched) ──
    print("\n" + "=" * 60)
    print("SCENARIO 2: Take only first 15 — stops after 2 pages")
    print("=" * 60)

    api2 = FakeUserAPI()
    premium_users = PaginatedCollection(
        lambda page: api2.get_page(page, page_size=10, filters={"plan": "premium"})
    )
    first_15 = premium_users.take(15)
    print(f"  Fetched {len(first_15)} premium users:")
    for user in first_15[:3]:
        print(f"    {user}")
    print(f"  ...")
    stats2 = premium_users.last_iterator_stats
    print(f"  Pages fetched to get 15 items: {stats2['pages_fetched']}")
    print(f"  API calls: {api2.total_fetches} (vs {math.ceil(50/10)} if fully loaded)")

    # ── Scenario 3: Active users only, small page size ──
    print("\n" + "=" * 60)
    print("SCENARIO 3: Active users only, page_size=5")
    print("=" * 60)

    api3 = FakeUserAPI()
    active_users = PaginatedCollection(
        lambda page: api3.get_page(page, page_size=5, filters={"active": True})
    )

    active_count = sum(1 for _ in active_users)
    print(f"  Active users: {active_count}")
    print(f"  Pages fetched: {active_users.last_iterator_stats['pages_fetched']}")

    # ── Scenario 4: Lazy chaining with standard Python itertools ──
    print("\n" + "=" * 60)
    print("SCENARIO 4: Standard library interop (itertools)")
    print("=" * 60)

    import itertools

    api4 = FakeUserAPI()
    all_users4 = PaginatedCollection(lambda page: api4.get_page(page, page_size=10))

    # islice lazily takes the first N — only fetches what's needed
    first_5 = list(itertools.islice(all_users4, 5))
    print(f"  First 5 (via islice): {[u.name for u in first_5]}")
    print(f"  Pages fetched: {all_users4.last_iterator_stats['pages_fetched']} (just 1!)")

    # Chain two filtered collections as if they were one
    api5 = FakeUserAPI()
    free_users    = PaginatedCollection(lambda p: api5.get_page(p, page_size=5, filters={"plan": "free"}))
    premium_users2= PaginatedCollection(lambda p: api5.get_page(p, page_size=5, filters={"plan": "premium"}))
    combined = list(itertools.chain(
        itertools.islice(free_users, 3),
        itertools.islice(premium_users2, 3)
    ))
    print(f"\n  Combined (3 free + 3 premium): {[u.name for u in combined]}")

Real-World Use Cases

1. Database Cursors

Every database driver's query result is an iterator. Python's sqlite3 cursor, Java's ResultSet, and Go's sql.Rows all implement the iterator protocol. Data is fetched in chunks from the database server rather than loaded all at once.

import sqlite3
conn   = sqlite3.connect(":memory:")
cursor = conn.execute("SELECT * FROM users WHERE active = 1")
for row in cursor:           # cursor IS an iterator
    process(row)

2. File System Traversal

os.walk() in Python, Files.walk() in Java, and filepath.Walk() in Go are tree iterators over file system hierarchies. They yield directories and files lazily without loading the entire directory tree into memory.

import os
for dirpath, dirs, files in os.walk("/var/log"):  # Lazy tree iterator
    for filename in files:
        process(os.path.join(dirpath, filename))

3. HTTP Pagination / API Clients

SDKs for AWS, GitHub, Stripe, and Salesforce wrap paginated API endpoints in iterators. The caller writes a simple for loop; the SDK handles cursor tracking, rate limiting, and page fetching.

# GitHub's PyGithub SDK
for commit in repo.get_commits():   # PaginatedList IS an iterator
    print(commit.sha)               # Fetches pages on demand

4. Stream Processing Pipelines

Kafka consumers, log processors, and ETL pipelines treat data as iterators. Each stage in the pipeline is a transforming iterator: parse → filter → enrich → validate → sink.

5. Lazy Data Generation

Generators in Python, Sequence in Kotlin, and lazy {} in Swift produce values on demand. Infinite mathematical sequences (fibonacci(), primes()) are only feasible as iterators — there's no "all Fibonacci numbers" to materialize.

def fibonacci() -> Iterator[int]:
    a, b = 0, 1
    while True:           # Infinite iterator
        yield a
        a, b = b, a + b

import itertools
first_10_fibs = list(itertools.islice(fibonacci(), 10))

6. UI Component Trees

React's virtual DOM reconciler, Android's RecyclerView.Adapter, and iOS's UITableViewDataSource are all iterator-like abstractions: give me item at index N, tell me the total count. The UI framework controls traversal; the data source provides items on demand.

7. Parser Token Streams

Lexers (tokenizers) return iterators of tokens. Parsers call next_token() to advance through the input stream without loading the entire file. This is how every compiler and interpreter processes source code.


Language-Specific Mistakes and Anti-Patterns

❌ Mistake #1: Returning self from __iter__ on the Collection

# BAD: Collection IS its own iterator — only one traversal at a time!
class BookShelf:
    def __init__(self):
        self._books = []
        self._pos   = 0  # Traversal state on the collection!

    def __iter__(self):
        self._pos = 0
        return self         # Returning self — the shelf IS the iterator

    def __next__(self):
        if self._pos >= len(self._books):
            raise StopIteration
        book = self._books[self._pos]
        self._pos += 1
        return book

# Now you can't nest loops or have two independent traversals:
for book1 in shelf:
    for book2 in shelf:   # Resets shelf._pos — outer loop gets confused!
        ...

# GOOD: Separate the collection from the iterator
class BookShelf:
    def __iter__(self):
        return ForwardBookIterator(self._books)  # New iterator each time!

❌ Mistake #2: Not Implementing __iter__ on the Iterator Itself

# BAD: Iterator doesn't implement __iter__ — can't be used in for-loops directly
class MyIterator:
    def __next__(self) -> Book:
        ...  # Only __next__, no __iter__

for book in MyIterator(books):  # TypeError: 'MyIterator' object is not iterable!

# GOOD: Iterator must implement both __iter__ (returning self) and __next__
class MyIterator:
    def __iter__(self):
        return self   # Iterators return themselves from __iter__

    def __next__(self) -> Book:
        ...

❌ Mistake #3: Consuming a Generator Twice

# BAD: Generators are exhausted after first iteration — second loop gets nothing
def sci_fi_books(shelf):
    for book in shelf:
        if book.genre == "Sci-Fi":
            yield book

sci_fi = sci_fi_books(shelf)

print(list(sci_fi))  # [Book1, Book2, Book3]
print(list(sci_fi))  # []  ← Empty! Generator was exhausted!

# GOOD: If you need to iterate multiple times, either:
# 1. Recreate the generator each time
sci_fi_list = list(sci_fi_books(shelf))   # Materialize into a list once
print(sci_fi_list)
print(sci_fi_list)  # Both work

# 2. Or wrap in a factory function / class with __iter__
class SciFiBooks:
    def __init__(self, shelf): self._shelf = shelf
    def __iter__(self):
        return (book for book in self._shelf if book.genre == "Sci-Fi")

Frequently Asked Questions

Q1: What is the difference between an Iterator and an Iterable?

They are complementary but distinct:

An Iterable is a collection that knows how to create an iterator. It has a factory method (__iter__, iterator(), [Symbol.iterator](), makeIterator()). You can iterate it multiple times — each call to the factory gives a fresh, independent iterator.

An Iterator is an object that does the traversal. It has state (current position) and advance methods (__next__, next(), hasNext()). Once exhausted, it's done — you typically don't reset it; you create a new one from the Iterable.

Rule of thumb: for (item in X) needs an Iterable. iter.next() gives you the next item from an Iterator.

Q2: Should I implement the language's built-in iterator protocol or define my own interface?

Always use the built-in protocol when possible. Python's __iter__/__next__, Java's Iterator<T>, JavaScript's Symbol.iterator, C#'s IEnumerable<T>, Swift's IteratorProtocol/Sequence, and Kotlin's Iterator<T>/Iterable<T> are all recognized by the language's for loop, spread operators, destructuring, and standard library functions. Custom interfaces only make sense for specialized traversal contracts (e.g., BiDirectionalIterator with hasPrevious()/previous()) that extend the standard protocol.

Q3: What is the difference between an external and internal iterator?

External iterator (pull-based): The client controls traversal by explicitly calling next(). More verbose but gives maximum control — you can pause, resume, read ahead, compare two iterators simultaneously, or exit early.

Internal iterator (push-based): The iterator controls traversal and calls a callback for each element (e.g., forEach((item) => process(item))). Simpler client code but less control — you can't easily pause mid-traversal or compare concurrent positions.

Most language for loops use external iterators under the hood. forEach, each, and reduce are internal iterators.

Q4: What is a lazy iterator and why does it matter?

A lazy iterator computes or fetches each element only when next() is called — not all at once upfront. Lazy iterators matter for:

  • Infinite sequences: fibonacci(), primes(), random_numbers() — these cannot be materialized
  • Large datasets: A database table with millions of rows should be streamed, not loaded into a List
  • Early exit: If you only need the first matching element, a lazy iterator stops after finding it without processing the rest
  • Memory efficiency: Processing 10GB of log files line by line uses constant memory; loading the whole file uses 10GB

Python's generators, JavaScript's generator functions, Rust's iterator adapters (.filter(), .map(), .take()), and Kotlin's Sequence are all lazy.

Q5: How do I implement a tree iterator that yields nodes in a specific order?

Traverse the tree iteratively using an explicit stack (for DFS) or queue (for BFS), rather than recursively — recursion depth is limited and recursive iterators are hard to pause:

class DepthFirstIterator:
    def __init__(self, root):
        self._stack = [root] if root else []

    def __iter__(self): return self

    def __next__(self):
        if not self._stack:
            raise StopIteration
        node = self._stack.pop()
        # Push children in reverse order so left child is processed first
        self._stack.extend(reversed(node.children))
        return node

class BreadthFirstIterator:
    def __init__(self, root):
        from collections import deque
        self._queue = deque([root]) if root else deque()

    def __iter__(self): return self

    def __next__(self):
        if not self._queue:
            raise StopIteration
        node = self._queue.popleft()
        self._queue.extend(node.children)
        return node

Q6: How do I safely iterate and modify a collection at the same time?

Never modify a collection while iterating it directly — most languages will throw a ConcurrentModificationException, a RuntimeError, or silently skip or repeat elements.

Safe approaches:

  1. Collect, then modify: Iterate to collect items to remove/modify, then apply changes after iteration is complete
  2. Iterator's own remove: Java's Iterator.remove() is the only safe way to remove during iteration
  3. Copy-then-iterate: Iterate a copy of the collection while modifying the original
  4. Build a new collection: Collect non-removed items into a new list instead of removing from the original
  5. Filter adapters: Rust's retain(), Python's list comprehensions, and Kotlin's filter() create a new filtered collection safely

Q7: How do I implement a bidirectional iterator?

Extend the standard iterator interface with hasPrevious() and previous() methods:

interface BidirectionalIterator<T> extends Iterator<T> {
    boolean hasPrevious();
    T previous();
}

class BidirectionalBookIterator implements BidirectionalIterator<Book> {
    private final List<Book> books;
    private int pos;

    BidirectionalBookIterator(List<Book> books, int startPos) {
        this.books = books;
        this.pos   = startPos;
    }

    public boolean hasNext()     { return pos < books.size(); }
    public Book    next()        { return books.get(pos++); }
    public boolean hasPrevious() { return pos > 0; }
    public Book    previous()    { return books.get(--pos); }
}

Java's ListIterator is a bidirectional iterator built into the standard library.

Q8: When should I use an iterator vs. returning a collection?

Return a collection (List, Set, Array) when:

  • The result is small and the caller will need random access (index-based lookup)
  • The result will be iterated multiple times
  • The caller needs .size() or .length() upfront
  • Thread safety and snapshot semantics matter (a List is a snapshot; an iterator may see live data)

Return an iterator/stream/sequence when:

  • The result could be very large or infinite
  • The caller will likely exit early (first match, first N items)
  • Generation is expensive and you want to defer computation
  • You're building a pipeline where each stage transforms and passes lazily to the next

Q9: How do I combine (merge) multiple iterators into one?

Use the built-in chain/concat utilities or implement a MergedIterator:

import itertools

# Merge N iterators sequentially
combined = itertools.chain(iter1, iter2, iter3)
for item in combined: process(item)

# Round-robin merge (interleave)
for item in itertools.chain.from_iterable(zip(iter1, iter2)):
    process(item)
function* chain<T>(...iterables: Iterable<T>[]): Generator<T> {
    for (const iterable of iterables) yield* iterable;
}

Q10: What is the Iterator pattern's relationship to functional programming concepts like map, filter, and reduce?

map, filter, and reduce are higher-order operations on iterators:

  • map returns a transforming iterator that applies a function to each element lazily
  • filter returns a filtering iterator that skips non-matching elements lazily
  • reduce/fold is a terminal operation — it consumes the iterator and produces a single value

In Rust, these are zero-cost iterator adapters: .map(f).filter(p).fold(init, g) compiles to a single loop with no intermediate allocations. In Kotlin, Sequence.map().filter().first() is lazy — only processes items until the first match. Python generators compose the same way: next(filter(predicate, map(transform, source))) never materializes intermediate collections.

The Iterator Pattern is the foundation on which these functional operations are built — they're all just iterators wrapping other iterators.


Key Takeaways

🎯 Core Concept: The Iterator Pattern provides a standard interface to sequentially access elements of a collection without exposing the collection's internal structure. The iterator object carries the traversal state independently of the collection, enabling multiple simultaneous traversals of the same collection in different directions or with different filters.

🔑 Key Benefits:

  • Decoupling: Algorithms written against the iterator interface work on arrays, trees, database cursors, network streams, and generated sequences without modification
  • Multiple Independent Traversals: Multiple iterators on the same collection maintain independent positions — nested loops and parallel scans work correctly
  • Lazy Evaluation: Fetch or compute elements on demand — process infinite sequences and large datasets with constant memory
  • Language Integration: Implementing the built-in iterator protocol unlocks for loops, spread, destructuring, and the entire standard library for free
  • Traversal Strategy Flexibility: Provide forward, reverse, filtered, tree-walk, and random iterators over the same collection without changing its interface

🌐 Language-Specific Highlights:

  • Python: Implement both __iter__ (returns self on iterators, returns new iterator on collections) and __next__; never put traversal state on the collection itself; generators are the most idiomatic iterator implementation
  • TypeScript: Implement [Symbol.iterator]() to unlock for...of, spread [...shelf], and destructuring const [first] = shelf; use generator functions (function*) for elegant lazy sequences
  • Java: Prefer the enhanced for-each loop (requires Iterable<T>); always call hasNext() before next(); use Iterator.remove() (never collection.remove()) for safe removal during traversal; be aware of ConcurrentModificationException
  • JavaScript: Implement both next() returning { value, done } and [Symbol.iterator]() returning this; generator functions (function* + yield) are the most idiomatic approach; never for...in a custom iterable (use for...of)
  • C#: Implement IEnumerable<T> + IEnumerator<T> pair; yield return in iterator blocks handles all the state machinery automatically; always using your enumerators to ensure Dispose() is called, especially for DB cursors and file streams
  • PHP: Implement all 5 methods of Iterator (current, key, next, rewind, valid); prefer IteratorAggregate for simple cases; never return $this from getIterator()
  • Go: Use pointer receivers for iterators with mutable state; implement HasNext()/Next() (Go has no built-in iterator protocol pre-1.23); use range with channels for concurrent producer-consumer iteration; Go 1.23+ adds iter.Seq[V] as the standard iterator type
  • Rust: Understand iter() (borrows, yields &T), iter_mut() (borrows mutably, yields &mut T), and into_iter() (moves, yields T); use for &book in &shelf to avoid move; iterator adapters (.map(), .filter(), .take()) are zero-cost abstractions — prefer them over manual loops
  • Dart: Extend Iterable<T> for collections (gives forEach, map, where, etc. for free); Iterator<T> for the raw moveNext()/current protocol; use yield and yield* in generator functions; List.reversed and .where() return lazy Iterables
  • Swift: Implement IteratorProtocol (requires mutating func next() -> Element?) for iterators; implement Sequence (requires makeIterator()) for collections; struct iterators require mutating on next(); conforming to Sequence gives map, filter, reduce, sorted, and contains for free
  • Kotlin: Implement Iterator<T> (requires hasNext()/next()) and Iterable<T> (requires iterator()) separately; use sequence {} builder with yield for lazy generators; prefer Sequence over List for long lazy pipelines to avoid intermediate allocations; for-in requires Iterable, not Iterator

📋 When to Use Iterator:

  • Traversal logic should be independent of collection structure (array, tree, graph, stream)
  • Multiple simultaneous independent traversals of the same collection are needed
  • You're working with large or infinite data sources that should be processed lazily
  • You need pluggable traversal strategies (forward, reverse, filtered, breadth-first, depth-first)
  • You want your custom collection to integrate with language for loops and standard library functions
  • You're building a data pipeline where each stage lazily transforms and passes to the next

⚠️ When NOT to Use Iterator:

  • Simple indexed array access with known size — a direct for (int i = 0; i < n; i++) is clearer and faster
  • The collection is tiny (< 10 items) and performance of overhead matters — direct method calls are simpler
  • Random access is needed (skip to index 500) — iterators are sequential; use the collection directly
  • The traversal is inherently recursive and would require an explicit stack — recursive traversal may be cleaner
  • All you need is a one-time forEach or map — use the collection's built-in methods instead

🛠️ Best Practices Across Languages:

  1. Never put traversal state on the collection: Collections should be reusable; iterators carry the ephemeral position
  2. Implement the language's native protocol: __iter__/__next__, Symbol.iterator, IEnumerable<T>, Sequence — always prefer native over custom interfaces for maximum interoperability
  3. Return a new iterator from every __iter__/iterator() call: Each call should produce a fresh, independent traversal
  4. Make filtering iterators lazy: Don't materialize a filtered copy — skip non-matching elements inside next()
  5. Protect infinite iterators with take(n) at the point of use: Never spread or collect an infinite iterator
  6. Use using/try-with-resources/defer for resource-holding iterators: DB cursors, file iterators, and network streams must be closed
  7. Prefer lazy sequences for long pipelines: Sequence (Kotlin), itertools chains (Python), Rust iterator adapters — avoid intermediate collections
  8. Never modify a collection during external iteration: Collect targets first, then modify, or use the iterator's built-in remove()

💡 Common Pitfalls:

  • Collection IS its own iterator: Returning self/this/$this from __iter__/getIterator() — destroys nested loop correctness
  • Missing __iter__ on custom iterators: Python iterators must implement both __iter__ (returning self) and __next__
  • Exhausted generator reuse: Generators are one-shot — consuming one, then iterating again gives nothing
  • for...in instead of for...of: for...in in JS/TS iterates over property keys; for...of uses Symbol.iterator
  • Not disposing resource iterators: DB cursors and file iterators that aren't closed leak connections and file handles
  • Modifying during iteration: ConcurrentModificationException (Java), RuntimeError (Python), undefined behavior elsewhere
  • Rust into_iter() consuming the collection: Use &collection or .iter() to borrow non-destructively
  • Kotlin Sequence vs List confusion: Sequence is lazy and single-pass; List is eager and reusable — mixing them incorrectly creates subtle bugs

🎁 Advanced Techniques:

  • Cursor-based pagination: Store the last-seen ID as a cursor rather than a page number — more efficient for large, live datasets and avoids items appearing twice or being skipped when data is inserted/deleted mid-iteration
  • Iterator fusion: Compilers (Rust, Haskell) and runtimes (Java Streams with spliterators) fuse adjacent .map().filter() calls into a single loop pass — zero-overhead abstractions
  • Parallel iterators: Rust's Rayon, Java's parallel streams, and Kotlin's asFlow().buffer() split iterators across threads — identical interface, parallel execution
  • Iterator coroutines: Kotlin Flow, Python async generators (async def + yield), and JavaScript async generators (async function*) make iterators first-class async citizens — for await (const item of asyncIter)
  • Spliterators: Java's Spliterator extends Iterator with trySplit() for parallel decomposition and estimateSize() for size hints — the foundation of Java parallel streams
  • Reactive streams: RxJava/RxJS bridge pull (Iterator) and push (Observer) by providing backpressure — the consumer signals demand, the producer emits at the requested rate

The Iterator Pattern is the single most universally implemented pattern in all of software — if you've ever written a for loop, you've used it. Understanding it deeply unlocks lazy evaluation, composable pipelines, and seamless integration with every language's standard library.


Want to dive deeper into design patterns? Check out our comprehensive guides on: