Python Iterators vs. Iterables

Distinguish iterables from iterators, use iter() and next() directly, handle exhaustion and build a custom iterator.

Lesson 41 of 107Advanced12 min read

An iterable is anything you can loop over, such as a list, string, dictionary, file or range. An iterator is the object that actually walks through it, one item at a time, remembering where it is. You get an iterator from an iterable with iter(), and take the next item with next():

Python
orders = ["rye", "sourdough"]     # iterable
stream = iter(orders)             # iterator
print(next(stream))               # rye

The distinction explains why you can loop over a list as often as you like but a generator or file only once. This lesson builds on iterators and generators: it spells out the two contracts, what happens at exhaustion, and how to write an iterator class with __iter__ and __next__. The examples follow a bakery working through its orders.

Iterable contract

An object is iterable if iter(obj) returns an iterator. In practice that means it defines __iter__(), or the older __getitem__() with integer indexes starting at 0. Every for loop begins by calling iter() on the thing you're looping over.

Being iterable promises only one thing: you can get the items in sequence. It does not promise indexing, a length or a second pass:

ObjectIterablelen()IndexingReusable
list, tuple, str, rangeyesyesyesyes
dict, setyesyesnoyes
file, generator, map()yesnonono

Functions that accept "any iterable", such as sum(), sorted(), list() and zip(), only use the iteration protocol, which is why they work on all of these. When your own function only needs to loop over its input once, write it the same way, and it will accept lists, sets, generators and files alike.

Iterator contract

An iterator has two methods:

  • __next__() returns the next item, or raises StopIteration when there are none left. The built-in next(it) calls it.
  • __iter__() returns the iterator itself, so an iterator is also iterable and can be used directly in a for loop.
Python
stream = iter(["rye", "sourdough", "spelt"])
print(next(stream))         # rye
for loaf in stream:         # continues from where next() left off
    print(loaf)             # sourdough, spelt

Each next() call advances the iterator; there's no way to step back or peek without consuming an item. That's why the for loop above starts at "sourdough": the first item was already taken. If you need to look ahead, store the item you took, or use itertools.tee() to split one iterator into several.

Exhaustion

When an iterator has no more items, next() raises StopIteration. A for loop catches that exception for you and simply ends. Calling next() yourself on an exhausted iterator raises it visibly, unless you give a default:

Python
stream = iter(["rye"])
print(next(stream))               # rye
print(next(stream, "finished"))   # finished, no exception
next(stream)                      # StopIteration

An exhausted iterator stays exhausted: looping over it again does nothing, silently. That catches people out with generators, map(), zip() and file objects. If you pass one of them to two functions in a row, the second gets nothing:

Python
squares = (n * n for n in range(4))
print(sum(squares))   # 14
print(max(squares, default=None))   # None: already used up

To use the values twice, convert to a list first, or create a fresh iterator.

Repeated traversal

Containers such as lists create a new, independent iterator every time you call iter() on them, which is why nested loops over the same list work and why you can loop over a list repeatedly. An iterator returns itself from iter(), so every loop over it shares one position.

Python
orders = ["rye", "spelt"]
print([(a, b) for a in orders for b in orders])   # 4 pairs: fresh iterators

stream = iter(orders)
print([(a, b) for a in stream for b in stream])   # [('rye', 'spelt')]: shared position

You can tell the two apart: iter(obj) is obj is True for iterators and False for reusable containers.

Some iterators never end, for example itertools.count() or a generator reading a live sensor. They're fine in a loop with a break, or with itertools.islice(), but passing one to list(), sum() or sorted() would run forever.

Independent practice and a worked check

Task: compare traversing a reusable list twice with consuming one generator twice.

Python
orders = ["rye", "spelt", "brioche"]
gen = (o.upper() for o in orders)

print("List, first pass: ", [o for o in orders])
print("List, second pass:", [o for o in orders])
print("Gen, first pass:  ", list(gen))
print("Gen, second pass: ", list(gen))
print(iter(orders) is orders, iter(gen) is gen)
Text
List, first pass:  ['rye', 'spelt', 'brioche']
List, second pass: ['rye', 'spelt', 'brioche']
Gen, first pass:   ['RYE', 'SPELT', 'BRIOCHE']
Gen, second pass:  []
False True

The last line shows why: the list hands out a fresh iterator, while the generator is its own iterator. For the worked check, a for loop over an exhausted iterator simply finishes: it receives StopIteration immediately and runs zero times.

Reference and next practice

The documentation defines iterator types and the glossary entries for iterable and iterator. Continue with magic methods and context managers. Practise with the Python exercises and Python quiz.

Iterators vs. Iterables Summary

  • Iterable contract: An iterable can provide an iterator through iter.
  • Iterator contract: An iterator supplies successive values through next and returns itself from iter.
  • Exhaustion: StopIteration signals that no next value remains.
  • Repeated traversal: Containers often create fresh independent iterators while a generator object is normally one iterator.

Iterators vs. Iterables Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Iterators vs. Iterables in a small program

Python
stream = iter(["rye", "sourdough"])
print(next(stream))
print(next(stream))
print(next(stream, "finished"))
Output
rye
sourdough
finished
Line-by-line explanation
  1. Line 1iter() turns the list into an iterator.
  2. Lines 2 to 3Each next() returns the following order and moves the position on.
  3. Line 4The iterator is exhausted, so next() returns the default instead of raising StopIteration.
Example 2

Writing an iterator class

Python
class OrderQueue:
    """Iterate over orders, highest priority first."""
    def __init__(self, orders):
        self.orders = sorted(orders, key=lambda o: o[1], reverse=True)

    def __iter__(self):
        return OrderIterator(self.orders)   # fresh iterator each time

class OrderIterator:
    def __init__(self, orders):
        self.orders = orders
        self.position = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.position >= len(self.orders):
            raise StopIteration
        order = self.orders[self.position]
        self.position += 1
        return order[0]

queue = OrderQueue([("rye", 1), ("wedding cake", 5), ("baguette", 3)])
print(list(queue))
print(list(queue))
Output
['wedding cake', 'baguette', 'rye']
['wedding cake', 'baguette', 'rye']
Line-by-line explanation
  1. Lines 6 to 7The container's __iter__ returns a new iterator, so the queue can be traversed repeatedly.
  2. Lines 14 to 15The iterator's __iter__ returns itself, as the protocol requires.
  3. Lines 17 to 22__next__ returns one item per call and raises StopIteration at the end.

Common Iterators vs. Iterables Mistakes

Reusing an exhausted iterator

Don't
lines = open("orders.txt")
total = sum(1 for _ in lines)
names = [line.strip() for line in lines]   # [] : file already read
Do
with open("orders.txt") as file:
    lines = file.readlines()
total = len(lines)
names = [line.strip() for line in lines]

A file object is its own iterator. Read it into a list if you need the lines more than once.

Calling next() without handling the end

Don't
first_urgent = next(o for o in orders if o.urgent)   # StopIteration if none
Do
first_urgent = next((o for o in orders if o.urgent), None)

Give next() a default when the iterator might be empty.

Iterators vs. Iterables in Real Projects

The iteration protocol in real code

The iteration protocol is why Python code can stay the same while the data source changes: a function written as for record in records: works with a list in tests, a CSV reader in production and a database cursor in a reporting job. Database libraries, csv.reader, os.scandir(), re.finditer() and API pagination helpers all return iterators so they don't load everything into memory at once.

Custom iterator classes are less common than generators, because a generator function implements the whole protocol automatically. They're still worth knowing for objects that hold state between items, and for understanding error messages such as TypeError: 'Order' object is not iterable, which means the class lacks __iter__.

Iterators vs. Iterables FAQ

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

An iterable can produce an iterator when you call iter() on it; lists, strings and dictionaries are iterables. An iterator produces the actual items with next() and keeps track of its position. Every iterator is iterable, but not every iterable is an iterator.

Is a list an iterator?

No. A list is an iterable. next(my_list) raises TypeError: 'list' object is not an iterator. Call iter(my_list) to get an iterator over it.

How do I reset an iterator in Python?

You can't rewind most iterators. Create a new one, by calling iter() on the original iterable again or calling the generator function again, or store the values in a list if you need several passes.

How do I check if an object is iterable?

The reliable test is to try iter(obj) and catch TypeError. isinstance(obj, collections.abc.Iterable) works for most objects but misses classes that only define __getitem__.

Should I write an iterator class or a generator?

Use a generator function for most cases; yield handles __iter__, __next__ and StopIteration for you in far fewer lines. Write a class when the iterator needs extra methods or state that callers can inspect.

Iterators vs. Iterables Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What happens to an exhausted iterator in a for loop?

Multiple choicemedium

Which review concern is valid for iterators vs. iterables?

Iterators vs. Iterables Quiz

2 questions to check what you learned.

  1. What happens to an exhausted iterator in a for loop?

    • It restarts
    • The loop finishes
    • Python ignores the distinction
  2. Which statement correctly describes exhaustion?

    • StopIteration signals that no next value remains.
    • Only the example output matters, not the rule
    • Every operation has the same behavior