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():
orders = ["rye", "sourdough"] # iterable
stream = iter(orders) # iterator
print(next(stream)) # ryeThe 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:
| Object | Iterable | len() | Indexing | Reusable |
|---|---|---|---|---|
| list, tuple, str, range | yes | yes | yes | yes |
| dict, set | yes | yes | no | yes |
file, generator, map() | yes | no | no | no |
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 raisesStopIterationwhen there are none left. The built-innext(it)calls it.__iter__()returns the iterator itself, so an iterator is also iterable and can be used directly in aforloop.
stream = iter(["rye", "sourdough", "spelt"])
print(next(stream)) # rye
for loaf in stream: # continues from where next() left off
print(loaf) # sourdough, speltEach 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:
stream = iter(["rye"])
print(next(stream)) # rye
print(next(stream, "finished")) # finished, no exception
next(stream) # StopIterationAn 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:
squares = (n * n for n in range(4))
print(sum(squares)) # 14
print(max(squares, default=None)) # None: already used upTo 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.
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 positionYou 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.
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)List, first pass: ['rye', 'spelt', 'brioche']
List, second pass: ['rye', 'spelt', 'brioche']
Gen, first pass: ['RYE', 'SPELT', 'BRIOCHE']
Gen, second pass: []
False TrueThe 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.
Iterators vs. Iterables in a small program
stream = iter(["rye", "sourdough"])
print(next(stream))
print(next(stream))
print(next(stream, "finished"))rye
sourdough
finishedLine-by-line explanation
- Line 1
iter()turns the list into an iterator. - Lines 2 to 3Each
next()returns the following order and moves the position on. - Line 4The iterator is exhausted, so
next()returns the default instead of raisingStopIteration.
Writing an iterator class
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))['wedding cake', 'baguette', 'rye']
['wedding cake', 'baguette', 'rye']Line-by-line explanation
- Lines 6 to 7The container's
__iter__returns a new iterator, so the queue can be traversed repeatedly. - Lines 14 to 15The iterator's
__iter__returns itself, as the protocol requires. - Lines 17 to 22
__next__returns one item per call and raisesStopIterationat the end.
Common Iterators vs. Iterables Mistakes
Reusing an exhausted iterator
lines = open("orders.txt")
total = sum(1 for _ in lines)
names = [line.strip() for line in lines] # [] : file already readwith 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
first_urgent = next(o for o in orders if o.urgent) # StopIteration if nonefirst_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.
What happens to an exhausted iterator in a for loop?
Which review concern is valid for iterators vs. iterables?
Iterators vs. Iterables Quiz
2 questions to check what you learned.
What happens to an exhausted iterator in a for loop?
- It restarts
- The loop finishes
- Python ignores the distinction
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