datarekha
Python Medium Asked at AmazonAsked at MicrosoftAsked at Meta

What is the difference between an iterable and an iterator in Python?

The short answer

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__
Iterablereturns a fresh iterator
Iteratorreturns selfnext 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.

Learn it properly Iterators

Keep practising

All Python questions

Explore further

Skip to content