An iterable is anything you can loop over — it knows how to produce an
iterator via __iter__. An iterator is the object that actually does
the walking: it has __next__ and yields one value at a time, remembering
its position. Every iterator is iterable (its __iter__ returns itself),
but not every iterable is an iterator.
nums = [1, 2, 3] # list: iterable, NOT an iterator
it = iter(nums) # iterator over the list
next(it) # 1 — iterators track position
next(it) # 2
Think of the iterable as the collection and the iterator as a cursor/bookmark into it. You can create many independent iterators from one iterable.
The iterator protocol is two methods. __iter__ must return the
iterator object itself, and __next__ returns the next value or raises
StopIteration when exhausted. That exception is the agreed signal
that there are no more items.
it = iter([10, 20])
it.__next__() # 10
it.__next__() # 20
it.__next__() # raises StopIteration
An iterable only needs __iter__ (returning a fresh iterator). An
iterator needs both. The StopIteration raise is what lets for loops
know when to stop — they catch it silently.
iter(obj) calls obj.__iter__() to get an iterator; next(it) calls
it.__next__() to advance it. next() accepts an optional default that
is returned instead of raising StopIteration when the iterator is
exhausted — handy for safe peeking.
it = iter("ab")
next(it) # 'a'
next(it) # 'b'
next(it, "done") # 'done' — default instead of StopIteration
# iter() also has a two-arg sentinel form:
# iter(callable, sentinel) calls until it returns sentinel
Use the default argument whenever you want to drain or sample an iterator
without wrapping next() in a try/except StopIteration.
Implement __iter__ (return self) and __next__ (return the next value
or raise StopIteration). The instance holds its own state between calls.
class Countdown:
def __init__(self, start):
self.n = start
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
list(Countdown(3)) # [3, 2, 1]
This works, but for most cases a generator function (using yield) is
far less boilerplate — it builds the __iter__/__next__/StopIteration
machinery for you. Reach for a class only when you need extra methods or
explicit state.
A for loop is sugar over the iterator protocol. Python calls iter() on
the iterable once to get an iterator, then repeatedly calls next() on it,
binding each result to the loop variable, until StopIteration is raised —
which it catches to end the loop.
for x in [1, 2, 3]:
print(x)
# is roughly equivalent to:
_it = iter([1, 2, 3])
while True:
try:
x = next(_it)
except StopIteration:
break
print(x)
This is why any object implementing the protocol "just works" in a for
loop, comprehension, or *-unpacking. The StopIteration is the hidden
handshake that terminates the loop.
An iterator is single-use / exhaustible: once __next__ has walked to
the end and raised StopIteration, it stays exhausted — there is no reset.
Re-iterating yields nothing. This trips people up with generators and
zip/map objects.
it = iter([1, 2, 3])
list(it) # [1, 2, 3]
list(it) # [] — already exhausted!
gen = (x for x in range(3))
sum(gen) # 3
sum(gen) # 0 — the generator is spent
A list (an iterable, not an iterator) can be looped many times because
each loop calls iter() to get a fresh iterator. If you need to reuse an
exhaustible result, materialize it into a list first.
iter(func, sentinel) builds an iterator that calls func() repeatedly until it
returns the sentinel value, then stops. It's perfect for reading streams in
fixed-size chunks without a while True/break.
# read a file in 1024-byte blocks until EOF (b''):
with open("data.bin", "rb") as f:
for chunk in iter(lambda: f.read(1024), b""):
process(chunk)
# roll a die until you get a 6:
import random
list(iter(lambda: random.randint(1, 6), 6))
Rule of thumb: use iter(callable, sentinel) to turn a "call until you see X" loop
into a clean iterator, especially for chunked I/O.
Make __iter__ return a fresh iterator each call (a generator or a new iterator
object) instead of self. Then the class is a reusable iterable, not a
single-use iterator.
class Squares:
def __init__(self, n): self.n = n
def __iter__(self):
return (i * i for i in range(self.n)) # new generator each time
sq = Squares(3)
list(sq) # [0, 1, 4]
list(sq) # [0, 1, 4] — works again!
Rule of thumb: return self from __iter__ for one-shot iterators; return a new
generator/iterator for collections meant to be looped repeatedly.
Regular slicing (it[1:3]) doesn't work on iterators. Use itertools.islice,
which lazily takes a range of items — essential for infinite or huge iterators.
from itertools import islice, count
islice(count(), 2, 5) # lazy: 2, 3, 4
list(islice(count(), 2, 5)) # [2, 3, 4]
list(islice(range(100), 10)) # first 10
Rule of thumb: islice is the iterator-friendly slice; it consumes (and discards)
skipped items and never materializes the whole sequence.
tee(iterable, n) returns n independent iterators over the same source. It
buffers items already consumed by one branch until the others catch up — useful when
you can't re-create the source.
from itertools import tee
it = (x * x for x in range(5))
a, b = tee(it, 2)
list(a) # [0, 1, 4, 9, 16]
list(b) # [0, 1, 4, 9, 16] — independent copy
# warning: don't keep using the original `it` after tee-ing it
Rule of thumb: use tee for limited multi-pass over a one-shot iterator, but if the
branches diverge a lot it buffers heavily — then a list is simpler.
Iterators have no built-in peek. Pull the value with next(), then push it back
by chaining it in front with itertools.chain — a common "lookahead" pattern.
from itertools import chain
it = iter([1, 2, 3])
first = next(it) # 1 — consumed
it = chain([first], it) # put it back on the front
list(it) # [1, 2, 3] — nothing lost
Rule of thumb: emulate peeking by next() + chain([val], it); for repeated
lookahead, wrap the iterator in a small buffering class.
You can't test emptiness without consuming an item. Use next(it, sentinel) and
compare against a unique sentinel; if you need the item, re-attach it with chain.
from itertools import chain
_sentinel = object()
def is_empty(it):
first = next(it, _sentinel)
if first is _sentinel:
return True, it
return False, chain([first], it) # give the item back
empty, it = is_empty(iter([])) # (True, ...)
Rule of thumb: there's no peek-free emptiness check — pull one item with a sentinel default and rebuild the iterator if it wasn't empty.
reversed(seq) returns an iterator that walks a sequence backwards. It needs a
sequence that supports __reversed__ or both __len__ and __getitem__ — so it
works on lists/tuples/ranges but not on plain generators or sets.
list(reversed([1, 2, 3])) # [3, 2, 1]
list(reversed(range(3))) # [2, 1, 0] — ranges are sized
reversed(x for x in range(3)) # TypeError — generators aren't reversible
class C:
def __reversed__(self): return iter([9, 8, 7])
list(reversed(C())) # [9, 8, 7] — custom hook
Rule of thumb: reversed needs a sized, indexable sequence (or __reversed__); for
one-shot iterables, materialize to a list first.
Since PEP 479 (default in 3.7+), a StopIteration that bubbles out of a generator
body is converted into a RuntimeError. This prevents a bug where an inner
next() raising StopIteration would silently end the generator.
def gen(it):
while True:
yield next(it) # when `it` is exhausted, next() raises StopIteration
list(gen(iter([1, 2]))) # RuntimeError: generator raised StopIteration
def safe(it):
for x in it: # use a for-loop, which handles StopIteration
yield x
Rule of thumb: inside generators, consume sub-iterators with for/yield from (or
next(it, default)), never a bare next() that can raise StopIteration.
Yes — in Python 3 map, filter, and zip return lazy iterators, not lists.
They compute on demand and are single-pass, so you must wrap them in list() to
see or reuse all results.
m = map(str.upper, ["a", "b"])
next(m) # 'A' — lazy, one at a time
list(m) # ['B'] — 'A' already consumed
list(filter(lambda x: x > 0, [-1, 2, -3, 4])) # [2, 4]
Rule of thumb: map/filter/zip are one-shot iterators in Python 3 — materialize
with list() if you need indexing, length, or multiple passes.
More Comprehensions & Iteration interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.