Of the 23 patterns in the catalog, this is the one you've already used today, because every for-each loop in every modern language runs on it. Iterator is the Gang of Four behavioral pattern whose official intent reads: provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation. The collection keeps its insides private, and traversal becomes somebody else's job.
An iterator is really just a bookmark object: it remembers where you are in a collection and hands you the next element when asked. The collection vends fresh bookmarks on demand, each loop holds its own, and the structure being walked, array, linked list, tree, or something lazier, stays a private implementation detail behind two small questions.
Here's the pattern's absence. A playlist stores song durations in a linked list whose head is public, and every function that needs the songs walks the nodes itself, pointer by pointer:
Both functions contain the same traversal loop, and so will the next ten functions anyone writes. The deeper cost is the coupling: every caller now knows the playlist is a linked list with fields named head and next, so the day profiling says an array would be faster, the change breaks every caller simultaneously. The collection's most private decision, how it stores things, has become everyone's business.
The iterator version makes the node structure private and exposes one thing: a way to get a bookmark. What's striking across the tabs is that every language has absorbed the pattern into syntax, each with its own spelling. Python's iterator protocol drives for/in, JavaScript's Symbol.iterator drives for/of, and Java's Iterator interface drives for-each. The example adds three durations, 210, 185, and 240. Before reading past the code, predict the total and count how many times the client touches a node:
The total is 635 and the client touches zero nodes, because the for-loop machinery pulls values through the bookmark while the linked list stays invisible. Swap the list for an array now and only the aggregate's own methods change. Note what exhaustion looks like per tab: Java throws NoSuchElementException past the end, Python generators raise StopIteration automatically, JavaScript yields done: true, and Rust's next() returns None, four costumes on the same contract.
Four roles. The Iterator interface declares the traversal contract. ConcreteIterators implement it while holding the position, like the Java tab's anonymous class with its current field or Rust's PlaylistIter. The Aggregate declares the iterator-vending method, and ConcreteAggregates (Playlist) return fresh iterators over their private structure. One aggregate can vend many iterator flavors, forward, reversed, filtered, each a different walk over the same data.
The pattern works like a museum audio guide: it leads you exhibit to exhibit, you never consult the floor plan, and two visitors with two guides can stand in different rooms of the same museum at once, which is exactly two iterators mid-walk over one collection. The analogy's limit is the museum's stillness. Collections get mutated mid-walk, and the Java documentation answers that case with the word unspecified: modify a collection during iteration by any route other than the iterator's own remove() and the behavior is formally anyone's guess.
Installing the pattern is four moves:
hasNext()/next(), __next__() with StopIteration, or next() returning an Option. The vocabulary varies, the contract doesn't.NoSuchElementException, Python raises StopIteration, JavaScript returns done: true, Rust returns None. Pick your language's convention, because the for-loop machinery is listening for exactly that signal.__iter__() or iterator() that hands out a new bookmark each time. Every for-loop gets its own, which is why nested loops over one collection just work.Iteration comes in two directions of control. External iteration pulls: the client holds the bookmark and calls next() when it wants more, which is what every for-each desugars to. Internal iteration pushes: the collection drives, calling your function once per element, the way forEach and map do. Pull composes better, since the client can stop, interleave two iterators, or hand the bookmark to someone else, while push is simpler to provide.
Generators are the trick that collapses the difference: you write push-style code, a plain loop with yield in it, and the language packages it as a pull iterator. Python's docs are explicit that a generator-based __iter__() supplies __next__() and StopIteration automatically, which is why the Python, C#, PHP, and TypeScript tabs contain no bookkeeping at all. The bookmark still exists, but the compiler is the one writing it.
Go held out against this pattern for over a decade, then absorbed it like everyone else. Go 1.23's range-over-func makes an iterator a function of type iter.Seq that pushes values to a yield callback until told to stop, and the range keyword now drives it. The Go blog names the push-pull split directly and ships iter.Pull for converting one to the other, which makes Go the language where the two iteration directions are most explicitly first-class.
Rust made the iterator its crown jewel. The Iterator trait demands one method, next() returning Option, and pays out roughly seventy adapter methods for it: map, filter, zip, chain, and the sum() the code tab uses instead of a loop. Every Rust for-loop desugars through IntoIterator into repeated next() calls, which means the GoF pattern isn't just available in Rust, it is the language's entire collection-processing model.
The protocol is two questions, and these five keep the answers trustworthy:
yield does for free: Python, C#, PHP, and JavaScript generators all implement the protocol from a plain loop body. Write the class only when the language offers nothing better, the way Java's anonymous iterator does.Two questions before you go. First: the client summed 635 without touching a node, so where does the traversal position live, and why does that location matter the moment two loops walk the playlist at once? Second: your for-each loop is external iteration, but a generator is written like internal iteration. What does the language do to bridge the two? Both answers are on this page.
Then go spot it in the wild, which takes about four seconds: every for/in, for/of, for-each, and range loop you can see in your editor right now is driving an iterator the language conjured. The more interesting hunt is the before-block's shape: find a class in your codebase whose callers reach into .items or .children and loop over the raw structure directly. Each of those call sites is a traversal that breaks the day the structure changes, and one __iter__() would orphan none of them.
class Node:
def __init__(self, value: int):
self.value = value
self.next: "Node | None" = None
class Playlist:
def __init__(self):
self.head: Node | None = None # public, and that's the problem
def total_duration(playlist: Playlist) -> int:
# The caller chases the playlist's pointers itself
total = 0
node = playlist.head
while node is not None:
total += node.value
node = node.next
return total
def print_titlesque(playlist: Playlist) -> None:
# And so does every other caller, identically
node = playlist.head
while node is not None:
print(node.value)
node = node.next
# Swap the linked list for an array tomorrow and every
# caller that ever touched .head and .next breaks at onceclass Node:
def __init__(self, value: int):
self.value = value
self.next: "Node | None" = None
class Playlist: # Aggregate: the node structure is private now
def __init__(self):
self._head: Node | None = None
self._tail: Node | None = None
def add(self, duration: int) -> None:
node = Node(duration)
if self._tail is None:
self._head = node
else:
self._tail.next = node
self._tail = node
def __iter__(self):
# A generator implements the iterator protocol for free:
# __next__ and StopIteration come built in
node = self._head
while node is not None:
yield node.value
node = node.next
# Example usage: the client never sees a node
playlist = Playlist()
playlist.add(210)
playlist.add(185)
playlist.add(240)
total = 0
for duration in playlist:
total += duration
print(total) # 635