What is the difference between an iterable and an iterator in Python?
An iterable implements __iter__ and returns an iterator. An iterator implements both __iter__ (returning itself) and __next__ (producing the next value or raising StopIteration). Every iterator is an iterable, but not every iterable is an iterator.
How to think about it
This question quietly sorts the candidates who think “for loops work on lists” from the ones who see the protocol underneath — the same two methods that make for, zip, map, and every comprehension behave identically on lists, files, and your own classes. It comes down to two dunder methods and the relationship between them.
An iterable knows how to hand you an iterator: it implements __iter__. An iterator is the thing that actually walks: it implements __next__ (produce the next value, or raise StopIteration) and an __iter__ that simply returns itself. So every iterator is an iterable, but not the reverse — a list is iterable, yet it is not its own iterator.
| Object | __iter__ | __next__ |
|---|---|---|
| Iterable | returns a fresh iterator | — |
| Iterator | returns self | next value, or raises StopIteration |
When Python runs for x in obj, it calls iter(obj) once to get an iterator, then calls next() on it again and again until StopIteration. That’s the whole mechanism — no magic.
A worked example
# A list is iterable but NOT an iterator — iter() hands back a separate object
nums = [10, 20, 30]
it = iter(nums)
print("iter(list) returns:", type(it)) # a distinct list_iterator
print("iter(list) is list:", it is nums) # False
# Driving it by hand is exactly what the for loop does
print("Manual next() calls:", next(it), next(it), next(it))
# A generator IS already an iterator — iter() returns itself
gen = (x**2 for x in range(4))
print("iter(gen) is gen:", iter(gen) is gen) # True
# The single-pass trap: an iterator, once drained, stays empty
it2 = iter([1, 2, 3])
print("First pass:", list(it2))
print("Second pass (empty!):", list(it2))
# A custom iterator: __iter__ returns self, __next__ ends with StopIteration
class Countdown:
def __init__(self, start):
self.n = start
def __iter__(self):
return self
def __next__(self):
if self.n < 0:
raise StopIteration
val = self.n
self.n -= 1
return val
print("Countdown:", list(Countdown(3)))
iter(list) returns: <class 'list_iterator'>
iter(list) is list: False
Manual next() calls: 10 20 30
iter(gen) is gen: True
First pass: [1, 2, 3]
Second pass (empty!): []
Countdown: [3, 2, 1, 0]
The split earns its keep in the fourth example. Because iter(nums) makes a fresh list_iterator each time, you can loop over the same list again and again. If the list were its own iterator, it could be consumed only once — which is exactly what happens to a generator, whose iter() returns itself.
Why the separation exists
Keeping the two roles apart is what lets you re-iterate a list freely: every for loop asks for a new iterator and starts from the top. Generators and file objects fold both roles into one object — they are their own iterator — which makes them lean but single-pass, and that is the source of one very common bug.