Iterables & Iterators
Every for loop in Python runs on two small protocols — being iterable and being an iterator — and understanding the difference explains a lot of otherwise-mysterious behavior.
An iterable is anything you can get an iterator from; an iterator is the object that actually remembers where you are and hands out one value at a time. Python's for loop, comprehensions, sum(), sorted(), unpacking, and many other features all quietly run on these two small protocols.
The Iterable Protocol
An object is *iterable* if it implements __iter__, a method that returns an iterator. Lists, tuples, strings, dicts, sets, files, and range objects are all iterable. Being iterable does not mean the object tracks its own position — it means it knows how to produce something that does.
Calling __iter__() on a list does not consume the list; it produces a fresh, independent iterator object.
numbers = [10, 20, 30]
print(hasattr(numbers, '__iter__')) # True -> numbers is iterable
iterator = numbers.__iter__()
print(type(iterator)) # <class 'list_iterator'>The Iterator Protocol
An object is an *iterator* if it implements both __iter__ (returning itself) and __next__ (returning the next value, or raising StopIteration when exhausted). Iterators are stateful and single-use: once exhausted, they stay exhausted.
iter(obj) calls obj.__iter__(); next(it) calls it.__next__(). Both are built-in functions that dispatch to the underlying dunder methods.
Every iterator is itself iterable (its __iter__ returns self), but not every iterable is an iterator — a list is iterable but is not itself an iterator, since it has no __next__.
numbers = [1, 2, 3]
it = iter(numbers)
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
next(it) # raises StopIterationiter() and next()
iter() and next() are the public entry points to these protocols; you rarely call __iter__/__next__ directly in application code.
next() accepts an optional default; when supplied, StopIteration is swallowed and the default is returned instead.
data = [5, 6]
it = iter(data)
print(next(it, 'done')) # 5
print(next(it, 'done')) # 6
print(next(it, 'done')) # 'done' - default suppresses StopIterationStopIteration
StopIteration is an ordinary exception class that signals exhaustion. It is not an error in the usual sense — it's the expected termination signal that for loops and other consumers catch automatically.
Inside a generator function, letting StopIteration escape uncaught used to end the generator silently, but PEP 479 (Python 3.7+) turns that into a RuntimeError to prevent bugs from being mistaken for normal completion.
it = iter([1])
next(it)
try:
next(it)
except StopIteration:
print('exhausted')How the for Loop Actually Works
A for loop is syntactic sugar. Python calls iter() once on the iterable to get an iterator, then calls next() repeatedly, catching StopIteration to know when to stop.
Call iter() on the iterable once, producing an iterator.
Call next() on that iterator each pass through the loop body.
Catch StopIteration to exit cleanly - no exception is ever visible to your code.
for x in [1, 2, 3]:
print(x)
# is equivalent to:
_it = iter([1, 2, 3])
while True:
try:
x = next(_it)
except StopIteration:
break
print(x)Writing a Custom Iterable/Iterator
A common pattern splits the two roles: a container class implements __iter__ and returns a separate iterator object, so multiple independent iterations over the same container can run at once.
Countdown is iterable (produces a fresh iterator each time you loop over it); CountdownIterator is the iterator (holds and mutates the position).
class Countdown:
def __init__(self, start: int) -> None:
self.start = start
def __iter__(self) -> 'CountdownIterator':
return CountdownIterator(self.start)
class CountdownIterator:
def __init__(self, current: int) -> None:
self.current = current
def __iter__(self) -> 'CountdownIterator':
return self
def __next__(self) -> int:
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
for n in Countdown(3):
print(n) # 3 2 1Iterable vs Iterator: Re-iterating
Because an iterable's __iter__ returns a new iterator each call, an iterable can be looped over repeatedly. An iterator, once handed out and consumed, cannot be reset - you must go back to the iterable and call iter() again.
nums = [1, 2, 3]
print(list(nums)) # [1, 2, 3] - iterable, works every time
print(list(nums)) # [1, 2, 3] - still works
it = iter(nums)
print(list(it)) # [1, 2, 3] - consumes the iterator
print(list(it)) # [] - already exhaustedIterators as Their Own Iterable
Because an iterator's __iter__ simply returns self, you can pass an iterator anywhere an iterable is expected - including into a for loop - and it behaves correctly, just without the ability to restart.
it = iter([1, 2, 3])
for x in it: # for calls iter(it), which returns it itself
print(x)Manual Iteration Patterns
Manual iter()/next() calls are useful for consuming part of an iterable and picking up later, or for pairing values from a single stream.
iter(callable, sentinel) repeatedly calls callable() and stops when the result equals sentinel - a lesser-known but handy two-argument form.
it = iter(range(6))
first_two = [next(it), next(it)]
rest = list(it)
print(first_two, rest) # [0, 1] [2, 3, 4, 5]
# iter() also accepts a callable + sentinel form:
import io
buf = io.StringIO('a\nb\nSTOP\nc\n')
for line in iter(buf.readline, 'STOP\n'):
print(line.strip()) # a, bRelated Concept: Generators
Generator functions and generator expressions are the easiest way to build an iterator without writing __iter__/__next__ by hand - Python generates that machinery for you from a function containing yield. See the dedicated Generators topic.
Edge Cases
in and unpacking (a, b, c = obj) also use the iterator protocol under the hood, so a custom iterable/iterator that follows the protocol correctly gets those features for free.
An object with only __getitem__ (no __iter__) can still be iterated in older-style code - Python falls back to calling obj[0], obj[1], ... until IndexError. This is legacy behavior; prefer implementing __iter__ explicitly in new code.
class Repeat3:
def __iter__(self):
return iter(['x', 'y', 'z'])
a, b, c = Repeat3()
print(a, b, c) # x y z
print('y' in Repeat3()) # TrueKey Takeaways
Iterable means "has __iter__"; iterator means "has __iter__ (returning self) and __next__".
iter(obj) produces an iterator; next(it) advances it and raises StopIteration when exhausted.
The for loop is sugar for calling iter() once and next() repeatedly, catching StopIteration automatically.
Iterators are single-use and stateful; iterables can typically be iterated over multiple times because each iteration gets a fresh iterator.
Generators are a concise way to build custom iterators without writing the dunder methods by hand.
it = iter([1, 2, 3]) for x in it: pass for x in it: # silently does nothing print(x) data = [1, 2, 3] for x in data: pass for x in data: # data is an iterable, gives a fresh iterator each time print(x)
Looping over an already-exhausted iterator produces no error and no items. Loop over the underlying iterable (or call iter() again) if you need to iterate more than once.
class Thing: def __next__(self):... # missing __iter__ class Thing: def __iter__(self): return self def __next__(self):...
for and iter() call __iter__ first; an object with only __next__ is not recognized as an iterator and raises TypeError when passed to iter().
def get_next(it): try: return next(it) except StopIteration: return None # ambiguous if None is a valid item
_sentinel = object() def get_next(it): val = next(it, _sentinel) return val if val is not _sentinel else None
Using next()'s default parameter with a private sentinel avoids conflating 'exhausted' with 'the next value happened to be None'.
What is the difference between an iterable and an iterator?
An iterable implements __iter__ and can produce an iterator on demand (possibly a new one each time); an iterator implements both __iter__ (returning itself) and __next__, and tracks its own position, consuming itself as it's advanced.
What does iter([1,2,3]) return, and what happens if you call it twice on the same list?
It returns a fresh list_iterator object each time; calling iter() twice on the same list gives two independent iterators, each starting from the beginning.
Why does a for loop never raise StopIteration visibly to your code?
Because the for loop's own machinery catches StopIteration internally as the signal to stop looping; it never propagates out of the loop.
Can an iterator be reset to the beginning?
Not directly - iterators are single-use and stateful. You must obtain a new iterator, typically by calling iter() on the original iterable again.
Why must __iter__ on an iterator return self?
So the iterator itself satisfies the iterable protocol and can be passed directly to for loops, iter(), or any function expecting an iterable.
What is the two-argument form of iter() used for?
iter(callable, sentinel) repeatedly calls callable() and stops when the returned value equals sentinel - useful for reading until a marker value, e.g. iter(file.readline, '').
Interview: why does Python separate the iterable and iterator roles instead of having one object do both?
Separating them allows a single iterable (like a list) to support multiple independent, concurrent iterations, each with its own position, rather than being limited to one traversal at a time.
Write a class Evens that is iterable and yields even numbers from 0 up to (but not including) a given limit, using a separate iterator class.
class Evens: def __init__(self, limit: int) -> None: self.limit = limit def __iter__(self) -> 'EvensIterator': return EvensIterator(self.limit) class EvensIterator: def __init__(self, limit: int) -> None: self.limit = limit self.current = 0 def __iter__(self) -> 'EvensIterator': return self def __next__(self) -> int: if self.current >= self.limit: raise StopIteration value = self.current self.current += 2 return value print(list(Evens(10))) # [0, 2, 4, 6, 8] list(Evens(10)) produces [0, 2, 4, 6, 8]; Evens can be iterated multiple times because __iter__ builds a new EvensIterator each call.
What does this print?
1 [2] [] next(it) consumes the first item (1). list(it) then drains the remaining items into a list ([2]) and leaves it exhausted, so the second list(it) yields an empty list.
What does this print, and why?
[1, 2, 3] [1, 2, 3] Weird.__iter__ builds a brand-new list iterator on every call, so each list(w) gets a fresh traversal - Weird is a well-behaved reusable iterable even though it delegates to a plain list's iterator internally.
it = iter([1, 2])
print(next(it))
print(list(it))
print(list(it))class Weird:
def __iter__(self):
return iter([1, 2, 3])
w = Weird()
print(list(w))
print(list(w))