Generator vs Iterator vs Iterable in Python: One Protocol
7 min readBytePatterns
Generator vs iterator vs iterable in Python: __iter__ hands out iterators, __next__ walks once, yield writes both for you. Laziness, exhaustion, memory.
Generator vs iterator vs iterable in Python in one line: an iterable is anything that can hand out an iterator when you call iter() on it, an iterator is the object that actually walks, handing out one item per next() until it raises StopIteration, and a generator is an iterator you write as a function with yield (or as a generator expression) instead of as a class. The three are not rivals but layers of one protocol: lists and your own containers are iterables, every for loop runs on an iterator, and generators are the shortest way to produce one. The protocol below is from the Python documentation, as of October 2026, and every example runs on Python 3.9.
| Term | What it has | Walk it twice? |
|---|---|---|
| iterable | iter, returning a new iterator | yes, each iter() starts over |
| iterator | next, and iter returning itself | no, one pass, then StopIteration forever |
| generator | an iterator built by a yield function or a generator expression | no, it is an iterator |
The problem it solves
You want for value in my_list: to work on your own linked list, tree or file reader, ideally without building a full Python list first. To do that you need to know which object holds the data, which object remembers where the walk is, and what happens when the walk ends. Mixing those up gives the classic bugs: a loop that silently does nothing the second time, or a nested loop that pairs only the first item.
The intuition
Separate the collection from the cursor.
- An iterable is the collection. It defines
__iter__, which returns a fresh iterator each time. A list, a dict, a string and a well-writtenLinkedListare iterables, and you can loop over them as often as you like. - An iterator is the cursor. It defines
__next__, which returns the next item or raisesStopIteration, and__iter__, which returns itself so that iterators can be used wherever an iterable is expected. The documentation adds the rule that matters: once__next__has raisedStopIteration, it must keep doing so. An exhausted iterator stays exhausted. - A generator is an iterator you do not have to write by hand. Calling a function that contains
yieldruns none of its body; it returns a generator object. Eachnext()runs the body up to the nextyield, hands out that value and freezes the local variables until the next call. When the function returns, the generator raisesStopIteration. The documentation points out the standard trick: if a container's__iter__is written as a generator, it automatically returns a new iterator on every call.
A for loop is this protocol in a few lines: call iter() on the expression once, then call next() until StopIteration, which the loop catches and treats as the end. yield from other delegates: it re-yields everything other produces, so one generator can chain several walks.
Laziness is the payoff. A generator computes each value when asked, so its memory does not depend on how many values it will produce, and it can describe sequences that would never fit in a list. The cost is the other side of the same coin: no len(), no indexing, no second pass, and errors surface only when the bad value is reached.
When to use which
- Your class holds data people will loop over: make it an iterable by writing
__iter__as a generator. - You need a stateful cursor with extra methods, such as
peek()orskip(): write an iterator class with__next__. - One pass over a large or endless stream, such as file lines, pages from an API or a running total: a generator function or expression.
- You need random access,
len(), or several passes: materialise alistonce. - You need to add and remove at both ends while walking: that is a buffer, not an iterator; use a
deque.
Watch it run
The lesson's animation is one pointer and one rule: keep reassigning it to node.next until it falls off the end. It searches 4 → 8 → 15 for 15, comparing each value and moving on, and returns index 2; it notes that jumping to the middle would already cost a walk, so halving buys nothing. Then it searches for 9: the same walk with no early exit, until node becomes empty, the list's built-in stop sign, and the loop returns -1. A generator-based __iter__ is exactly that walk with a yield where the comparison was. The empty pointer at the end becomes StopIteration, and the search moves out into whatever loop consumes the values.
Traversal and Search
Step 1 of 11
Traversal is one pointer and one rule: keep reassigning it to node.next until it falls off the end.
The same interactive animation as the lesson — step through it with the controls.
The code
A linked list whose __iter__ is a generator, next to the same walk written as an iterator class. One restarts on every loop; the other is used up after one:
class Node:
def __init__(self, v):
self.value, self.next = v, None
class LinkedList: # an iterable: it can hand out iterators
def __init__(self, values):
self.head = None
for v in reversed(values):
node = Node(v)
node.next, self.head = self.head, node
def __iter__(self): # a generator function: each call, a fresh walk
node = self.head
while node is not None:
yield node.value
node = node.next
class Walker: # the same walk as a hand-written iterator
def __init__(self, head):
self.node = head
def __iter__(self):
return self # an iterator is its own iterator
def __next__(self):
if self.node is None:
raise StopIteration
value, self.node = self.node.value, self.node.next
return value
chain = LinkedList([4, 8, 15])
it = iter(chain) # what a for loop calls first
print(type(it).__name__, iter(it) is it, iter(chain) is it) # generator True False
print(next(it), next(it), next(it)) # 4 8 15
try:
next(it)
except StopIteration:
print("exhausted") # exhausted
print(list(chain), list(chain)) # [4, 8, 15] [4, 8, 15] <- the iterable restarts
w = Walker(chain.head)
print(list(w), list(w)) # [4, 8, 15] [] <- the iterator does not
Next, what laziness buys and what it costs: memory, silent exhaustion, in consuming items, yield from, and an error that waits until it is reached. The sizes are for 64-bit CPython 3.9:
import sys
squares_list = [n * n for n in range(1_000_000)] # built now, all of it
squares_gen = (n * n for n in range(1_000_000)) # built on demand, one at a time
print(sys.getsizeof(squares_list), sys.getsizeof(squares_gen)) # 8448728 112
print(sum(squares_gen) == sum(squares_list)) # True
print(sum(squares_gen)) # 0 <- already exhausted, no error
def evens(it):
for x in it:
if x % 2 == 0:
yield x
g = evens(chain)
print(8 in g, 4 in g, list(g)) # True False [] <- 'in' consumed 4 on the way to 8
def walk_both(a, b):
yield from a # delegate: re-yield everything a produces
yield from b
print(list(walk_both(chain, LinkedList([16, 23])))) # [4, 8, 15, 16, 23]
lazy = (1 / x for x in [1, 0]) # nothing runs yet
print(next(lazy)) # 1.0
try:
next(lazy)
except ZeroDivisionError as e:
print("raised on demand:", e) # raised on demand: division by zero
The list's container alone is about 8 MB, before counting the integers it points to; the generator object stays the same small size whatever the range. The second sum returned 0 rather than raising, which is how exhaustion bugs hide. Last, a seeded check that the generator walk, the lesson's while loop and a plain list agree, that nested loops over the iterable work, and that a search written over the iterator matches list.index:
import random
def walk_loop(head): # the lesson's walk, collecting values
out, node = [], head
while node is not None:
out.append(node.value)
node = node.next
return out
def find(chain, target): # search written over the iterator
return next((i for i, v in enumerate(chain) if v == target), -1)
rng = random.Random(2)
ok = True
for _ in range(3000):
values = [rng.randint(0, 9) for _ in range(rng.randint(0, 12))]
chain = LinkedList(values)
ok &= list(chain) == walk_loop(chain.head) == values
ok &= [(a, b) for a in chain for b in chain] == [(a, b) for a in values for b in values]
t = rng.randint(0, 10)
ok &= find(chain, t) == (values.index(t) if t in values else -1)
print(ok) # True
shared = Walker(LinkedList([1, 2]).head) # one iterator used in a nested loop
print([(a, b) for a in shared for b in shared]) # [(1, 2)] <- the inner loop drained it
The nested loop over chain worked because each for called __iter__ and got its own generator. The same loop over one Walker produced a single pair: the inner loop drained the only cursor.
The complexity
- A full walk:
O(n)time for all three; a generator adds a small constant per item for resuming its frame. - Memory: a list holds all
nitems,O(n); an iterator or generator holds only its position,O(1), plus whatever the underlying collection already uses. - First item: a generator yields it after
O(1)work; building a list first costsO(n)before you see anything, which matters for early exits likefind. - Random access and
len(): not available on iterators;list(it)costsO(n)time and memory.
Where it goes wrong
- Looping over a generator twice. The second loop runs zero times without an error. Materialise with
list()or call the generator function again. - Making a container its own iterator. If
__iter__returnsselfand the cursor lives on the container, nested loops and concurrent walks share one position. Return a new iterator, and a generator makes that automatic. len()on a generator. It raisesTypeError; there is no length until the walk is done.x in genas a check. It consumes items up to the match, so later code sees a shorter stream.- Assuming errors appear at creation. A generator expression runs nothing until asked, so a bad value fails wherever it is first consumed, often far from where it was written.
- Changing a linked list during a walk. The generator follows live
nextpointers; unlinking the current node mid-walk can end the walk early or skip values.
When it shows up in interviews
As a Python fundamentals question ("what is the difference between an iterator and an iterable?", "what does yield do?") and as a follow-up to any linked-list or tree problem: "make your class work in a for loop" or "return the values lazily". In-order traversal of a binary tree with yield from on both subtrees is a favourite. The walk itself, including the off-by-one traps of search, is in linked list traversal and search in Python, and when you need a buffer rather than a one-way stream, deque vs list for queues covers the right structure. The pointer walk is the Traversal and Search lesson.
How to say it in an interview
"An iterable is anything with __iter__ that gives me a fresh iterator; an iterator has __next__, returns itself from __iter__, and raises StopIteration when it is done, after which it stays done. A for loop calls iter() once and then next() until StopIteration. A generator is the easy way to write an iterator: a function with yield whose state is frozen between calls. For a linked list I write __iter__ as a generator that walks from the head, so the list stays reusable, nested loops work, and memory stays O(1) per walk."
Frequently asked questions
What is the difference between an iterator and an iterable in Python?
An iterable can produce iterators: calling iter() on it returns a new one each time, so it can be looped over repeatedly. An iterator is the cursor itself: next() returns one item at a time until StopIteration, and once exhausted it stays exhausted. Every iterator is also an iterable that returns itself.
Is a generator an iterator in Python?
Yes. A generator object, created by calling a function that contains yield or by a generator expression, implements both __iter__ and __next__. It is a single-pass iterator: it computes values lazily, one per next() call, and raises StopIteration when the function body finishes.
Why does my generator return nothing the second time?
A generator is an iterator, and iterators are single-pass. The first loop consumed every value; the second calls next() on an exhausted generator, gets StopIteration immediately and runs zero times. Store the results in a list, or call the generator function again to get a fresh generator.
Sources
- Built-in Types: Iterator Types and Generator Types — Python Documentation
- Glossary: iterable, iterator, generator — Python Documentation
- Expressions: yield expressions and generator expressions — Python Documentation
- Compound statements: the for statement — Python Documentation
- Classes tutorial: iterators and generators — Python Documentation