Handbooks / Python / Chapter 7
Iterators & Generators
53 pages · ~101 min✓ Reviewed
Builds on Files & JSON.
Part 1 · Iterators & Generators: How Python Walks Through Data
Iterators & Generators: How Python Walks Through Data
Every for loop you have ever written rests on one small contract. Unpacking, list(), sum(), the in operator, *args splatting and every comprehension rest on it too. That contract is the iteration protocol: ask an object for an iterator, then call next() on it until it says it is done. Once you see the protocol, a lot of Python stops looking like magic. It also explains why some loops silently produce nothing the second time around.
It matters in production because iteration is where memory and time are won or lost. A list comprehension over ten million items builds all ten million values before you touch the first. A generator hands them out one at a time and holds almost nothing. The same machinery lets you stream a multi-gigabyte log, stop at the first match, or describe an endless sequence safely. It also hides some of the nastiest quiet bugs in the language, such as a generator that is exhausted and returns zero instead of raising an error.
By the end of this chapter you will be able to tell an iterable from an iterator, and write both by hand. You will turn a function into a generator with yield, read a paused generator's frame, and steer it with send, throw and close. You will build constant-memory pipelines out of generator expressions and itertools, and delegate work with yield from. You will also know the trade-offs well enough to pick between a list, a generator expression, a generator function and an iterator class, and to spot the classic traps before they ship.
You need Python 3.11 or newer and nothing else, because every example uses only the standard library. You should be comfortable with functions, for loops, lists and dicts, and basic exceptions with try and except. Run the examples in a REPL or a scratch file as you read, and call next() by hand on anything that looks surprising.
Part 2 · Iterable vs Iterator: The Core Distinction
Two Different Things
Python's for loop never talks to your data directly. It talks to a second object that walks over the data, and that is where two words that are easy to blur come from. An iterable is any object that can hand you such a walker. Technically it is an object that implements __iter__(). An iterator is the walker itself, the object that actually produces values one at a time. It implements both __iter__() and __next__().
The two roles overlap in one direction only. Every iterator is also an iterable, because its __iter__() simply returns self. The reverse is false. Most iterables, including lists, tuples, dicts and strings, are not iterators, because they have no __next__() and no memory of a position.
| Iterable | Iterator | |
|---|---|---|
| Implements | __iter__() | __iter__() and __next__() |
| Job | Creates an iterator on request | Produces the values, one per call |
__iter__() returns | A new iterator each time | Itself (self) |
| Remembers a position | No | Yes |
| Can be looped over twice | Yes | No, it exhausts |
| Example | [1, 2, 3] | iter([1, 2, 3]) |
The built-in iter() turns an iterable into an iterator, and next() pulls one value out of an iterator. Handing a plain list to next() fails, because the list is the container and not the cursor. The example below shows that failure, then does it properly with iter().
nums = [1, 2, 3] try: next(nums) except TypeError as e: print(e) it = iter(nums) print(type(it).__name__) print(next(it), next(it), next(it)) try: next(it) except StopIteration: print('StopIteration')
'list' object is not an iterator list_iterator 1 2 3 StopIteration
- 1Iterablethe list, file or range you wrote after in
- 2iter(obj)calls iter and builds a fresh iterator
- 3next(it)called again and again, one value per call
- 4StopIterationthe signal that the loop is finished
next(my_list) raises TypeError, because a list is iterable but not an iterator. Wrap it first with iter(my_list) and keep the result in a variable. If you write next(iter(my_list)) each time, you get a brand-new iterator and the first item every time.
Reuse, Exhaustion and the Containers You Know
The split between the two roles explains a behaviour that surprises newcomers. An iterable can be looped over any number of times, because each for loop asks it for a fresh iterator. An iterator is single-use. Each next() moves its position forward and nothing ever moves it back, so once it reaches the end it stays exhausted for good.
Because the position lives in the iterator and not in the list, you can also have several independent cursors over the same data. The example below loops a list twice, drains one iterator, and then makes two iterators that do not disturb each other.
nums = [1, 2, 3] for _ in range(2): print([n * 10 for n in nums]) it = iter(nums) print(list(it)) print(list(it)) print(next(it, 'empty')) m1, m2 = iter(nums), iter(nums) print(next(m1), next(m1), next(m2)) print(m1 is m2, iter(m1) is m1, iter(nums) is nums)
[10, 20, 30] [10, 20, 30] [1, 2, 3] [] empty 1 2 1 False True False
Read the last line closely. m1 is m2 is False because each call to iter(nums) built a separate cursor. iter(m1) is m1 is True, which is the rule that an iterator is its own iterator. iter(nums) is nums is False, because a list is not its own iterator. The second list(it) returned [] without any error. An exhausted iterator is silent, which is why this bug is easy to miss.
You already use many iterables: list, tuple, dict, set, str, range, file objects and collections.deque. All of them can be used in a for loop, but only some of them are iterators. The next example asks Python directly, using an in-memory StringIO as a stand-in for a file.
from collections import deque from collections.abc import Iterable, Iterator import io things = { 'list': [1], 'tuple': (1,), 'dict': {'k': 1}, 'set': {1}, 'str': 'ab', 'range': range(3), 'file': io.StringIO('x\n'), 'deque': deque([1]), } for name, obj in things.items(): print(f'{name:6} Iterable={isinstance(obj, Iterable)!s:5} Iterator={isinstance(obj, Iterator)}')
list Iterable=True Iterator=False tuple Iterable=True Iterator=False dict Iterable=True Iterator=False set Iterable=True Iterator=False str Iterable=True Iterator=False range Iterable=True Iterator=False file Iterable=True Iterator=True deque Iterable=True Iterator=False
File objects are the odd one out. A file keeps a read position, so it is its own iterator, and looping over it a second time gives nothing unless you seek(0) or reopen it. Every other container in the list hands out a new iterator per loop. What each one yields differs, as the table shows.
| Container | Yields when iterated | Indexable | Has len |
|---|---|---|---|
list, tuple, deque | The items in order | list and tuple yes, deque yes but slow in the middle | Yes |
dict | The keys | By key, not by position | Yes |
set | The items, in no promised order | No | Yes |
str | Single-character strings | Yes | Yes |
range | Integers, computed on demand | Yes | Yes |
| file object | One line at a time | No | No |
range(10) is not a generator and not an iterator. It is a lazy sequence. It stores only start, stop and step, yet it supports indexing, len(), membership tests and unlimited reuse. The next example makes that visible.
Testing the Roles and Keeping the Picture
The example below compares range with a generator expression, which is a true iterator. Indexing and len() work on the first and fail on the second. Asking iter() for a range gives a separate range_iterator object, so the range itself is never used up.
r = range(10) print(len(r), r[3], r[-1], 7 in r) print(list(r)[:3], list(r)[:3]) print(iter(r) is r, type(iter(r)).__name__) g = (x for x in range(10)) try: len(g) except TypeError as e: print(e) try: g[3] except TypeError as e: print(e)
10 3 9 True [0, 1, 2] [0, 1, 2] False range_iterator object of type 'generator' has no len() 'generator' object is not subscriptable
To check at runtime which role an object plays, use the abstract base classes in collections.abc. isinstance(x, Iterable) asks whether iter(x) can work, and isinstance(x, Iterator) asks whether x can also be advanced with next(). These checks look for the methods themselves, so a class of your own passes without inheriting from anything.
from collections.abc import Iterable, Iterator class Shelf: def __init__(self, items): self.items = items def __iter__(self): return iter(self.items) s = Shelf([1, 2]) g = (x for x in s) print(isinstance(s, Iterable), isinstance(s, Iterator)) print(isinstance(iter(s), Iterator), isinstance(g, Iterator))
True False True True
Shelf defines only __iter__(), so it counts as an iterable and not as an iterator. Each iter(s) call returns a fresh list iterator, which is exactly the behaviour a re-readable container should have. The generator g is an iterator without our having written any __next__() ourselves.
isinstance(x, Iterable) only looks for __iter__. An old-style class that supports looping solely through __getitem__ fails the check, yet iter(x) and for still work on it. When in doubt, call iter(x) and catch TypeError.
An iterable is the book, and an iterator is the bookmark. One book can hold many bookmarks, and each one walks forward through the pages exactly once. A bookmark that reaches the last page is finished. To read again, you take a new bookmark from the book.
Part 3 · The iter/next Protocol Mechanics
What iter() and next() Actually Call
Python's iteration model is a handshake between two built-in functions and two dunder methods. Calling iter(obj) asks the object for a fresh iterator, and calling next(it) asks that iterator for its next value. Both built-ins are thin wrappers: they find the matching method and call it for you.
| You write | Python runs | Result |
|---|---|---|
iter(obj) | type(obj).__iter__(obj) | an iterator |
next(it) | type(it).__next__(it) | the next value, or StopIteration |
next(it, default) | same call, but catches StopIteration | the value, or default |
iter(func, sentinel) | calls func() until it returns sentinel | an iterator over the results |
Notice the word type in both the first and second rows. Python looks the method up on the class, never on the instance. This is the same rule that applies to every special method, and it means __getattr__ and instance attributes cannot supply __iter__. In the example below, the class has a __getattr__ that would happily answer any attribute request, and we even assign __iter__ onto the instance. iter() ignores both.
class Ghost: def __getattr__(self, name): print('getattr', name) return lambda: iter([1, 2]) g = Ghost() g.__iter__ = lambda: iter([1, 2]) try: iter(g) except TypeError as e: print(e)
The lookup goes to Ghost, not to g, so neither the instance attribute nor __getattr__ is consulted.
'Ghost' object is not iterableNow the two calls working together on a two-character string. Each next advances the same iterator by one position. Once the characters run out, next raises StopIteration. Passing a second argument to next turns that exception into an ordinary return value, which is handy when an iterator might legitimately be empty.
it = iter('ab') print(next(it)) print(next(it)) try: next(it) except StopIteration: print('StopIteration') print(next(iter([]), 'empty'))
a b StopIteration empty
The Fallback, the for Loop and the Sentinel Form
The iterator protocol is not the only way in. If an object has no __iter__ but does define __getitem__, iter() quietly builds a sequence iterator for it. That iterator calls __getitem__ with the integers 0, 1, 2 and so on, and treats an IndexError as the end of the data. This is the older protocol, kept so that classic sequence-like classes still work in loops.
class Squares: def __getitem__(self, i): if i >= 4: raise IndexError return i * i print(hasattr(Squares, '__iter__')) print(list(Squares()))
False [0, 1, 4, 9]
With both halves of the protocol in hand, a for loop is easy to desugar. Python calls iter() once, before the loop starts. It then calls next() over and over, runs the body after each value, and leaves the loop the moment StopIteration appears. After the body finishes, control goes back to the next() call.
Written out by hand, the same loop is a while True with a try/except. The break on StopIteration is the entire exit mechanism.
data = [10, 20] it = iter(data) while True: try: x = next(it) except StopIteration: break print(x)
10 20
The two-argument form iter(callable, sentinel) is a different way to make an iterator. It has no container at all: it just calls callable() with no arguments on every step and stops as soon as a result equals the sentinel. It is the classic way to read a file in fixed-size blocks, because read() returns an empty bytes object at end of file.
import io f = io.BytesIO(b'abcdefghij') for blk in iter(lambda: f.read(4), b''): print(blk)
b'abcd' b'efgh' b'ij'
StopIteration, Exhaustion and Who Drives the Protocol
StopIteration is a control-flow signal, not an error. It is how an iterator says "I have nothing more to give", and the machinery that drives iteration expects it and swallows it. That is why a normal for loop ends silently instead of crashing. It only looks like an error when you call next() yourself and forget to handle it.
There is one strict rule attached to it: an exhausted iterator must stay exhausted. Once __next__ has raised StopIteration, every later call must raise it again, forever. Consumers rely on this, because a loop that has ended, or a second consumer that arrives late, must never see fresh values appear.
it = iter([1]) print(next(it)) for _ in range(3): print(next(it, 'done'))
1
done
done
doneA __next__ that resets its counter after reaching the end breaks the invariant. A second loop, or any helper that probes the iterator once more, then gets data it should never have received. Forgetting to raise StopIteration at all is just as bad, since every consumer then loops forever.
Loops are only one of many consumers. Anything that takes an iterable calls iter() and then next() the same way. The tracer class below prints every step so that you can watch unpacking and the in operator drive it. Unpacking two names asks for a third value on purpose, to make sure nothing is left over. The in test stops pulling the moment it finds a match.
class Trace: def __init__(self, n): self.n = n self.i = 0 def __iter__(self): print('iter') return self def __next__(self): if self.i >= self.n: print('next -> stop') raise StopIteration self.i += 1 print('next ->', self.i) return self.i a, b = Trace(2) print(a, b) print(2 in Trace(3))
iter next -> 1 next -> 2 next -> stop 1 2 iter next -> 1 next -> 2 True
| Construct | How it drives the protocol |
|---|---|
a, b = pair | calls iter(), takes exactly two items, then one more next() to confirm the end |
list(x), sum(x) | call iter() once and pull until StopIteration |
v in x | pulls items and compares each, stopping early on a match |
f(*x) | pulls everything into the argument tuple |
| comprehensions | run an implicit for loop over iter() and next() |
Where the Protocol Came From
The protocol was introduced by PEP 234 in Python 2.2. Before it, looping meant indexing with __getitem__, which is why that fallback survives today. PEP 234 gave every object one uniform way to say "here is how I am walked", and the same two methods underpin every loop construct in the language.
- 1iter(obj)looks up iter on the type, or falls back to getitem with 0, 1, 2
- 2next(it)looks up next on the type of the iterator
- 3Values floweach call returns one item and moves the cursor forward
- 4StopIterationsignals the end and keeps being raised on every later call
Everything that iterates in Python is the same loop: get an iterator once, pull with next(), stop on StopIteration. The rest of this guide builds on that.
Part 4 · Writing an Iterator Class by Hand
The Minimal Iterator Class
A generator is the easy way to build an iterator, but the protocol underneath is only two methods. If you write them yourself you see exactly what the interpreter relies on. An iterator class needs __iter__, which returns the object itself, and __next__, which hands out the next value or signals the end.
The end is signalled by raising StopIteration. A for loop catches that exception and quietly leaves the loop, so it acts as control flow and not as a failure. Everything the iterator remembers between calls lives in instance attributes, so you move the cursor forward by hand.
- 1Check the guardis the cursor at or past the end?
- 2Raise StopIterationonly if the guard fired
- 3Advance the cursorupdate the attributes
- 4Return the valuethe one for this step
Here is the smallest useful version, a counter that produces 0 up to n - 1. The guard comes first, then the cursor moves, then the old value is returned.
class Count: def __init__(s, n): s.i, s.n = 0, n def __iter__(s): return s def __next__(s): if s.i >= s.n: raise StopIteration s.i += 1 return s.i - 1 c = Count(3) print(next(c), next(c), next(c)) try: next(c) except StopIteration: print('done') print(list(Count(4)))
The cursor is s.i; nothing else tracks position.
0 1 2 done [0, 1, 2, 3]
Everything in this class is bookkeeping: the two attributes, the guard, the increment and the subtraction to return the old value. A generator would hide all of it. That verbosity is the price of owning the cursor, and it is the reason hand-written iterators are the exception and not the default.
If __next__ never raises StopIteration, nothing ever tells the consumer to stop. A for loop, list() or sum() over it runs forever and hangs the interpreter, and list() also eats memory until it is killed.
from itertools import islice class Forever: def __init__(s, n): s.i, s.n = 0, n def __iter__(s): return s def __next__(s): s.i += 1 # the guard is missing return s.i - 1 print(list(islice(Forever(3), 6)))
islice cuts it off safely so we can watch the bug.
[0, 1, 2, 3, 4, 5]
The object was built with n = 3, yet it happily produced six values and would produce more. Bounding an unknown iterator with islice while you debug is a safe way to find out whether the guard is missing.
Keeping Containers Re-Iterable
Suppose you write a container class, a shelf of items, and you want people to loop over it. The tempting shortcut is to put __next__ on the container and have __iter__ return self. It works for the first loop. The second loop then starts with the cursor already at the end, and it does not raise any error.
The fix is the separate-class pattern. The container's __iter__ builds and returns a new iterator object every time it is called. The container holds the data, and each loop gets its own cursor with its own state.
| Container returns self | Container returns new iterator | |
|---|---|---|
| Where the cursor lives | On the container itself | On a separate iterator object |
| Second for loop | Sees nothing, no error | Starts from the beginning |
| Two loops at once | Fight over one cursor | Each has an independent cursor |
| Verdict | Anti-pattern | Correct for containers |
The example below defines one cursor class and uses it two ways. Shelf is the correct container, and BadShelf is the anti-pattern, because it inherits __iter__ returning self, so the container and the iterator are the same object.
class ShelfCursor: def __init__(self, items): self.items = items self.pos = 0 def __iter__(self): return self def __next__(self): if self.pos >= len(self.items): raise StopIteration self.pos += 1 return self.items[self.pos - 1] class Shelf: def __init__(self, items): self.items = items def __iter__(self): return ShelfCursor(self.items) class BadShelf(ShelfCursor): pass shelf = Shelf(['a', 'b', 'c']) print(list(shelf)) print(list(shelf)) a, b = iter(shelf), iter(shelf) print(next(a), next(a), next(b)) bad = BadShelf(['a', 'b', 'c']) print(list(bad)) print(list(bad))
['a', 'b', 'c'] ['a', 'b', 'c'] a b a ['a', 'b', 'c'] []
Shelf can be looped over any number of times, and two cursors made with iter(shelf) move independently. BadShelf yields everything once and then an empty list forever, with no exception to point at the cause.
A container whose __iter__ returns self looks fine in a quick test and breaks only when the data is walked twice. Nothing is raised, so the bug often shows up as a total that is mysteriously zero. Keep the data in the container and put the cursor in a separate iterator object.
ABCs, When Classes Win, and the Generator Comparison
The module collections.abc removes some boilerplate. If your class inherits from collections.abc.Iterator, the abstract base class supplies __iter__ for you (it returns self), and you only write __next__. Forgetting __next__ then fails at instantiation instead of at the first loop.
Hand-written iterators still earn their keep when the iterator is an object in its own right. A class can carry extra methods such as a reset, expose its state as plain attributes you can print, and be pickled, which a running generator cannot. The next example shows all three on one small class.
import pickle from collections.abc import Iterable, Iterator class Countdown(Iterator): def __init__(self, start): self.n = start def __next__(self): if self.n <= 0: raise StopIteration self.n -= 1 return self.n + 1 def reset(self, start): self.n = start c = Countdown(3) print(list(c)) c.reset(2) print(list(c)) print(isinstance(c, Iterable), isinstance(c, Iterator)) d = Countdown(5) next(d) print(d.n) # state is visible e = pickle.loads(pickle.dumps(d)) # and picklable print(next(e))
[3, 2, 1] [2, 1] True True 4 4
The isinstance checks pass because Iterator is itself a subclass of Iterable. Those ABCs also check structure: any class that defines __iter__ counts as an Iterable, even without inheriting. A class that is iterable only through the old __getitem__ protocol is the exception, and registration fixes it. Without register, isinstance checks and type-driven code disagree with what iter() actually accepts.
from collections.abc import Iterable class Legacy: def __getitem__(self, i): if i >= 2: raise IndexError return i * 10 print(isinstance(Legacy(), Iterable)) print(list(Legacy())) Iterable.register(Legacy) print(isinstance(Legacy(), Iterable))
False [0, 10] True
Now compare the class with the generator. The behaviour is the same, and a generator function gets it from the compiler: it is an iterator the language writes for you, with the cursor, the guard and StopIteration all handled.
def count(n): for i in range(n): yield i print(list(count(4)), list(count(4)) == list(Count(4)))
Three lines against twelve; Count is the class from the first page.
[0, 1, 2, 3] True
| Form | Lines | Failure mode | Wins when |
|---|---|---|---|
| Iterator class | ~12 | No StopIteration means a hang | You need methods, visible state, pickling or reset |
| Generator function | ~3 | Exhausts once | You only need the loop |
Hand-write an iterator when the cursor is something other code will touch. Keep the cursor on a separate object from the container, inherit from Iterator to skip __iter__, and always guard __next__ with a StopIteration.
Part 5 · Generator Functions and yield
One keyword changes what a call does
A function becomes a generator function the moment the keyword yield appears anywhere in its body. The yield can sit inside a loop, behind an if, or on a line that never runs. The compiler marks the code object when it compiles the function, so the decision is made before anything executes.
The surprising part is what a call does. Calling a generator function runs none of its body. Python builds a generator object, hands it back, and stops. That object is already an iterator: it has __iter__, which returns itself, and __next__, which runs the body up to the next yield. You write neither method, because the compiler supplies both.
The example below makes the delay visible. The print('a') and print('b') lines are inside the body, so they appear only when next() drives the generator. The two lines printed before the first next() show that the call itself produced no output.
from collections.abc import Iterator def gen(): print('a') yield 1 print('b') yield 2 g = gen() print('created', type(g).__name__) print(isinstance(g, Iterator), iter(g) is g) print(next(g)) print(next(g))
Nothing from the body prints until the first next()
created generator True True a 1 b 2
Read the output in order. The call gen() printed nothing, and the generator object passed the Iterator check with no methods written by us. The first next(g) ran the body from the top, printed a, and stopped at yield 1. The value 1 came back to the caller, and the caller's print showed it. The second next(g) printed b and returned 2.
Writing setup() for a generator function and expecting its side effects is a classic bug. Opening a file, validating arguments or logging a start message inside the body all wait until the first next(). If the generator is never advanced, that code never runs and no error appears. Do argument checks in a normal wrapper function when they must fail early.
Suspend, resume and the frame behind it
yield does three things at once. It suspends the running frame and hands one value to the caller. It preserves every local variable and the exact position in the code. On the next next(), execution continues right after the yield expression, with all locals as they were. Control moves back and forth between caller and generator, and each side runs only while the other waits.
- 1next(g)caller asks for a value
- 2Body runsfrom the saved position
- 3yield valueframe freezes, value goes out
- 4Caller continuesgenerator waits, locals kept
The comparison with an ordinary function shows why this works. A normal return ends the call and destroys the frame, so its locals are gone. A yield leaves the frame alive. The generator object holds a reference to it on the heap until the generator finishes or is discarded.
| return in a normal function | yield in a generator | |
|---|---|---|
| Frame afterwards | Destroyed | Frozen on the heap, kept by the generator |
| Locals | Lost | Kept for the next resume |
| Position in code | Gone | Saved, resumes after the yield |
| How many times it hands out a value | Once | As many times as it yields |
The standard library lets you inspect this. inspect.getgeneratorstate reports the lifecycle stage, and gi_frame.f_locals shows the paused variables. Once the generator finishes, gi_frame becomes None because the frame has been released.
from inspect import getgeneratorstate def running_total(xs): total = 0 for x in xs: total += x yield total g = running_total([5, 10, 20]) print(getgeneratorstate(g)) next(g) next(g) print(getgeneratorstate(g), g.gi_frame.f_locals['total']) list(g) print(getgeneratorstate(g), g.gi_frame)
GEN_CREATED GEN_SUSPENDED 15 GEN_CLOSED None
A fresh generator is GEN_CREATED. After two next() calls it sits suspended at the yield inside the loop, and total is still 15, though no function call is active. Draining it with list(g) closes it and drops the frame. This is why a generator is best seen as a resumable function: cooperative multitasking in miniature, where the function chooses when to pause. It is not a container, and no collection of values sits behind it.
How a generator ends, and what return does
A generator has two ways to finish. The simple one is to fall off the end of the body. Python then raises StopIteration for you, which is the signal for loops, list() and friends wait for. The other is return value. The value is not yielded. It is stored on the exception as StopIteration.value.
There are two ways to read that value. You can catch the exception yourself and look at e.value. Or you can delegate with yield from, which swallows the StopIteration and evaluates to the returned value. The example shows both endings and the delegation case.
def job(): yield 'step 1' yield 'step 2' return 'done' g = job() print(next(g)) print(next(g)) try: next(g) except StopIteration as e: print('returned', e.value) def plain(): yield 1 p = plain() next(p) try: next(p) except StopIteration as e: print('plain', e.value) def outer(): result = yield from job() print('outer got', result) print(list(outer()))
step 1 step 2 returned done plain None outer got done ['step 1', 'step 2']
The generator that falls off the end reports None, while job reports 'done'. In outer, yield from job() passes both steps through to list(). It also captures 'done' into result, and list() never sees that value. A for loop ignores StopIteration.value in the same way, so return is useful only to callers who go looking for it.
return x inside a generator does not produce x for the consumer. A loop over job() shows only the two steps and never 'done'. If a value must reach a plain for loop, yield it. Keep return for a final result that a yield from caller will pick up.
Several yields in a body, yields inside while or for loops, and yields inside try/finally are all legal and common. The try/finally case matters for resources, because the finally block runs when the generator is closed early as well as when it finishes.
def reader(): try: yield 'line 1' yield 'line 2' finally: print('cleanup') g = reader() print(next(g)) g.close() print('closed')
line 1
cleanup
closedWe read one line and then called close(). Python raised GeneratorExit at the paused yield, the finally block ran, and only then did control return to us. Without close(), the cleanup would wait until the generator is garbage collected, and that timing is not something to rely on for files or sockets.
A yield anywhere makes a generator function. Calling it runs nothing and returns an iterator. Each next() thaws the frozen frame, runs to the next yield, and freezes it again. Ending the body raises StopIteration, carrying the return value if there is one.
Part 6 · Inside the Generator Object: State & Frames
What a generator object really is
When you call a generator function, nothing in its body runs. Python builds a generator object that owns a paused frame, which holds the function's local variables and a marker for where execution stands. Every next() thaws that frame, runs it to the next yield, and freezes it again. This page opens the object up so you can see that machinery.
The four attributes
A generator exposes its internals through four gi_ attributes. They are read-only views, useful for debugging and for understanding what the interpreter is holding on your behalf.
| Attribute | Holds | Typical value |
|---|---|---|
gi_frame | The suspended frame, or None once the generator has finished | a frame object, then None |
gi_code | The code object compiled from the function body | running_total.__code__ |
gi_running | True only while the body is executing right now | False almost always |
gi_yieldfrom | The sub-iterator being delegated to by yield from, else None | another generator, or None |
Four lifecycle states
Every generator is in exactly one of four states. You never set the state yourself. It follows from what has been called on the object, and inspect.getgeneratorstate(g) reports it as a string.
- 1CREATEDGEN_CREATED: called, body not started
- 2RUNNINGGEN_RUNNING: body executing now
- 3SUSPENDEDGEN_SUSPENDED: paused at a yield
- 4CLOSEDGEN_CLOSED: finished, raised, or close()d
A generator moves between RUNNING and SUSPENDED once per item. It leaves for CLOSED when the body returns, when an exception escapes, or when close() is called. CLOSED is permanent: a closed generator never runs again.
import inspect def counter(n): i = 0 while i < n: yield i i += 1 g = counter(3) print(inspect.getgeneratorstate(g)) next(g) print(inspect.getgeneratorstate(g)) list(g) print(inspect.getgeneratorstate(g)) print(g.gi_frame)
GEN_CREATED
GEN_SUSPENDED
GEN_CLOSED
NoneOnce the generator is exhausted, gi_frame becomes None. The frame has been released, which is the last time its locals were reachable through the generator.
Frames, flags and why locals survive
The flag that changes what a call does
The compiler sets the CO_GENERATOR flag in the code object's co_flags for any function whose body contains yield. A normal call creates a frame and executes it. When the interpreter sees this flag, it creates the frame, wraps it in a generator object, and returns that object without running a single line of the body. That one bit is the whole difference between def f(): return 1 and def g(): yield 1.
Why locals survive suspension
An ordinary function call frame is thrown away on return. A generator's frame is heap-allocated and the generator object keeps a reference to it. As long as the generator is alive, so is the frame, and with it every local variable and the saved position in the bytecode. Nothing special is copied on yield. The frame simply stays put, and next() resumes it.
Peeking at a paused generator
Because the frame is still there, you can read it. gi_frame.f_locals maps local names to their current values, and gi_frame.f_lasti is the index of the last bytecode instruction executed, which tells you how far in the body the generator has progressed. The example below also checks the flag on a plain function and a generator function, and shows gi_yieldfrom pointing at a delegate.
import inspect def running_total(items): total = 0 for x in items: total += x yield total def plain(): return 1 def outer(): yield from running_total([1]) g = running_total([5, 10, 20]) print(g.gi_code.co_name, g.gi_running, g.gi_yieldfrom) next(g) next(g) print(g.gi_frame.f_locals['total'], g.gi_frame.f_locals['x']) print(g.gi_frame.f_lasti > 0) print(bool(plain.__code__.co_flags & inspect.CO_GENERATOR), bool(running_total.__code__.co_flags & inspect.CO_GENERATOR)) o = outer() next(o) print(o.gi_yieldfrom.gi_code.co_name)
running_total False None 15 10 True False True running_total
When a pipeline stalls, g.gi_frame.f_locals shows the loop variable and accumulators of a paused stage without adding any print statements. Treat it as a read-only inspection tool, not an API to build logic on.
close(), GeneratorExit and cleanup
What close() does
Calling g.close() on a suspended generator throws GeneratorExit into the frame at the yield where it is paused. If the generator does not catch it, the exception propagates out, close() swallows it, and the state becomes CLOSED. The one rule is that the generator must not yield another value after receiving GeneratorExit. It should clean up and let the exception go, or simply return.
Cleanup blocks run for you
Since GeneratorExit is a real exception raised inside the body, try/finally and with blocks around the paused yield unwind normally. The same thing happens when the last reference to a suspended generator disappears and it is garbage collected, because the finalizer closes it first. The example nests a with inside a try/finally to show the unwinding order: inner blocks run first.
import inspect from contextlib import contextmanager @contextmanager def opened(name): print('enter', name) try: yield name finally: print('exit', name) def rows(name): with opened(name) as f: try: yield f + ':1' yield f + ':2' finally: print('rows finally') g = rows('log') print(next(g)) g.close() print(inspect.getgeneratorstate(g))
enter log log:1 rows finally exit log GEN_CLOSED
Putting the resource inside the generator under with or try/finally makes cleanup possible. Calling close(), or wrapping the generator in contextlib.closing, makes it happen at a moment you chose.
Things that go wrong: misuse and lingering frames
Two errors from breaking the rules
| Misuse | What Python raises |
|---|---|
Yielding after receiving GeneratorExit | RuntimeError: generator ignored GeneratorExit |
Calling next() on a generator that is already running | ValueError: generator already executing |
The first follows from the close contract: close() expects the generator to stop, so a further yield is a refusal and Python reports it. The second is non-reentrancy. A generator has a single frame and that frame is mid-execution, so asking it to resume itself would corrupt its state. This shows up when a generator, directly or through a callback, tries to pull from itself.
def stubborn(): try: yield 1 except GeneratorExit: yield 2 g = stubborn() next(g) try: g.close() except RuntimeError as e: print(e) def selfish(): yield next(me) me = selfish() try: next(me) except ValueError as e: print(e)
generator ignored GeneratorExit generator already executing
Reference cycles delay cleanup
A suspended frame keeps everything its locals refer to alive. If one of those references leads back to the generator itself, you have a reference cycle. Reference counting alone can never free it, so the generator's finally block waits until the cyclic garbage collector happens to run. In the example below, the generator's argument holds a dict that holds the generator. Dropping our names does nothing visible until gc.collect().
import gc gc.disable() def holder(box): try: yield finally: print('released') box = {} g = holder(box) box['g'] = g next(g) del g, box print('deleted') gc.collect() print('collected')
deleted released collected
If a generator opens a file and gets caught in a cycle, or is abandoned on a runtime without prompt reference counting, the handle stays open until some later collection. Close generators explicitly with close(), contextlib.closing, or by exhausting them. Do not assume finally fires the moment you stop using the generator.
A bare except: or except BaseException: around a yield catches GeneratorExit. If the handler then yields, close() raises the RuntimeError above. Catch Exception instead, or re-raise GeneratorExit after cleanup.
Part 7 · Lazy Evaluation: Compute on Demand
What Lazy Means and What It Saves
A computation is lazy when its value is produced only at the moment someone asks for it. Until then the work is deferred, and if nobody ever asks, the work is never done at all. A generator is the standard example. Calling a generator function runs none of its body, and each next() runs just far enough to reach the next yield.
The example below makes that visible. The generator prints a message each time it computes a value. Watch where the messages land relative to the calls that consume the values.
def noisy_squares(n): for i in range(n): print(f"computing {i}") yield i * i gen = noisy_squares(3) print("created, nothing computed yet") print(next(gen)) print("between calls") print(next(gen))
Work happens inside next(), not at creation
created, nothing computed yet computing 0 0 between calls computing 1 1
The third value was never computed, because nobody asked for it. That is deferral in its purest form: the cost of an item is paid only by the consumer who wants it.
Memory: one frame plus one item
A paused generator holds exactly two things: its suspended frame (the local variables and the position where it stopped) and the single item it is about to hand over. Nothing about the items already delivered or the items still to come is stored. The space is O(1), whether the stream has ten items or ten billion.
A list comprehension takes the opposite approach and builds every element before you can touch the first one. For [x*x for x in range(10_000_000)] that means ten million int objects plus an 80 MB pointer array, roughly 400 MB in total. The generator expression (x*x for x in range(10_000_000)) is a few hundred bytes, because it has done nothing yet.
| Form | Evaluation | Memory for 10M squares |
|---|---|---|
[x*x for x in range(10_000_000)] | Eager, all at once | About 400 MB |
(x*x for x in range(10_000_000)) | Lazy, on demand | A few hundred bytes |
The generator never stores the stream. It stores the recipe for the next item and the place where it left off. Memory stays flat as long as the consumer drops each item after using it.
Stopping Early and Streams With No End
Short-circuiting: stop at the first match
Laziness pays off most when you do not need the whole stream. next(x for x in huge if pred(x)) pulls items only until the predicate first succeeds, then stops. An eager version would filter every item before you could look at the first hit. The counter in this example shows how few items get examined out of ten million.
checked = 0 def is_big(x): global checked checked += 1 return x > 1000 hit = next(x for x in range(10_000_000) if is_big(x)) print(hit, checked)
The scan ends at the first match, not at the last item
1001 1002
Only 1002 items were tested, and the other 9,998,998 were never produced. If no item matches, next raises StopIteration. Pass a default, as in next(gen, None), when a miss is a normal outcome.
Infinite streams become expressible
An eager container must be finite, because it has to exist in memory. A lazy producer has no such limit, since it only ever makes the next value. A while True loop around a yield is a perfectly valid, safe definition of an endless sequence. The danger lives only in the consumer, which must not try to swallow all of it, as list(naturals()) or sum(naturals()) would.
You bound an infinite stream from the consuming side. itertools.islice takes a fixed number of items, and a plain break leaves the loop when you have seen enough. Either way the generator is simply left suspended.
from itertools import islice def naturals(): n = 0 while True: yield n n += 1 print(list(islice(naturals(), 10))) for n in naturals(): if n * n > 50: break print(n)
Two ways to bound an endless stream
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 8
| Bounding tool | Where the limit lives | Best when |
|---|---|---|
islice(stream, n) | A count known up front | You want exactly n items |
break in the loop | A condition on the items | The stop point depends on the data |
next(x for x in stream if pred(x)) | The first match | You want one answer |
Streaming Files and Composing Pipelines
A file object is already a lazy stream
Iterating a file object, as in for line in f, yields one line at a time and reads from disk in small buffered chunks. The whole file never has to fit in memory. f.readlines() does the opposite: it loads every line into one list, which on a multi-gigabyte log means exhausting RAM before the first line is processed.
Writing for line in f.readlines(): looks harmless but builds the full list first. Write for line in f: instead. The loop body is identical and the memory use drops from the size of the file to the size of one line.
Stacking stages into a pipeline
Generator expressions can be chained, each one reading from the one before it. Writing lines = (l.strip() for l in f) and then nums = (int(l) for l in lines if l.isdigit()) builds two stages, and no data moves yet. Only when a consumer such as sum(nums) asks for a value does the request travel down the chain, and each stage pulls exactly one item from the stage beneath it.
- 1file fyields one raw line
- 2linesstrip() the line
- 3numskeep digits, int()
- 4sum()asks for the next value
To see the interleaving, the next example uses a small generator function for the first stage so it can report each read. An in-memory text stream stands in for the file.
import io def stripped(f): for raw in f: print("read", repr(raw)) yield raw.strip() f = io.StringIO("10\nabc\n20\n") nums = (int(l) for l in stripped(f) if l.isdigit()) for n in nums: print("got", n)
Reads and results alternate; nothing is read ahead
read '10\n' got 10 read 'abc\n' read '20\n' got 20
The line abc was read and rejected inside the pipeline without the loop body ever seeing it. At no point did more than one line exist at once, so sum(nums) over this chain is a single pass with O(1) memory, however big the file is.
Each stage is cheap to write and does nothing alone. The consumer at the end sets the pace, and that pace propagates all the way back to the file.
The Price of Laziness
Latency versus throughput
Going lazy changes when you get results, not how much total work there is. The first result arrives almost immediately, because only the work for one item has to happen. This matters for a progress display, for a stream you may abandon, or for a pipeline feeding a network client. The full pass still touches every item, and each item now pays a small resume-and-suspend toll, so total throughput can come out slightly lower than a tight list comprehension.
| Metric | List comprehension | Generator |
|---|---|---|
| First item | After all n items are built | After one item is built |
| Full pass | O(n) | O(n), a little slower per item |
| Memory | O(n) | O(1) |
For small data that is walked more than once, the list is both faster and simpler. Laziness earns its keep when the data is large, unbounded, or arrives over I/O, where holding it all would be expensive or impossible.
Harder to reason about and debug
Eager code runs in the order it is written. Lazy code runs in the order it is consumed, which can be far from where the expression appears. Side effects such as logging, counters and network calls fire later than you expect, and any names the expression reads are looked up at consume time. A traceback also points at the line doing the consuming, not at the stage that actually failed. The example shows both timing surprises.
log = [] def tag(x): log.append(x) return x lazy = (tag(x) for x in [1, 2, 3]) print(log) list(lazy) print(log) limit = 2 small = (x for x in range(5) if x < limit) limit = 4 print(list(small))
Side effects and closed-over names resolve at consume time
[] [1, 2, 3] [0, 1, 2, 3]
The log stayed empty until the generator was consumed. The filter also used limit = 4 rather than the 2 that was current when the expression was written, because the condition is evaluated lazily. The outermost iterable, here range(5), is the one part evaluated immediately.
Putting a print, a database write or a counter increment inside a generator expression and expecting it to run when the line executes. It runs only when something consumes the generator, and only once per item. If the effect must happen now, use a loop or a list comprehension.
Stay lazy for large, unbounded or I/O-driven data that you walk once. Materialize a list when you must iterate twice, index, or sort. Keep side effects out of lazy stages so that order of evaluation does not matter.
Part 8 · Generator Expressions & Comprehension Family
Syntax, Parentheses and What Runs at Creation
A generator expression is a list comprehension with its brackets swapped for parentheses: (expr for x in iterable if cond). The clauses mean the same thing in both forms. The difference is when the work happens. The list comprehension builds every result immediately. The generator expression builds a generator object and produces one value each time something asks for the next one.
Parentheses are optional when the genexp is the sole argument of a call, so sum(x*x for x in data) is valid and is the usual way to write it. If the call has a second argument, the genexp needs its own parentheses. Otherwise Python cannot tell where the expression ends, and you get a SyntaxError.
data = [1, 2, 3, 4, 5, 6] squares = [x * x for x in data if x % 2 == 0] lazy = (x * x for x in data if x % 2 == 0) print(squares) print(type(lazy).__name__) print(sum(x * x for x in data)) def scale(items, factor): return [i * factor for i in items] print(scale((x for x in data), 10)) try: compile('scale(x for x in data, 10)', '<s>', 'eval') except SyntaxError: print('SyntaxError')
Brackets versus parentheses, the sole-argument shortcut, and the two-argument rule
[4, 16, 36] generator 91 [10, 20, 30, 40, 50, 60] SyntaxError
Lazy, except for the outermost iterable
Nearly everything inside a genexp waits until you consume it. The exception is the outermost iterable, the one after the first in. Python evaluates it immediately, at the moment the genexp is created, and calls iter() on it right then. Only the filter, the expression and any inner for clauses are deferred.
- 1Creationoutermost iterable is evaluated and iter() is taken
- 2Generator object returnedno filter or expression has run yet
- 3Consumptioneach next() runs the if-test and the expression for one item
The consequence is that the generator holds a reference to the original list object, not to the variable name. Rebinding the name afterwards changes nothing for the generator. Mutating that same list object, on the other hand, is visible to it. A non-iterable outermost value fails at creation, not at first next().
mylist = [1, 2, 3] g = (x for x in mylist) mylist = [] print(list(g)) nums = [1, 2, 3] h = (n for n in nums) nums.append(4) print(list(h)) try: (x for x in 5) except TypeError: print('TypeError at creation')
Rebinding the name does not matter; mutating the object does
[1, 2, 3] [1, 2, 3, 4] TypeError at creation
Late Binding and Choosing the Right Form
Closed-over names are read at consume time
The outermost iterable is captured early, but every other name in the genexp is looked up when each value is produced. The filter and the expression are closures over the enclosing scope. If you rebind a name between creating the generator and consuming it, the generator sees the new value. The same rule explains the classic lambda-in-a-loop surprise, because the lambdas all share one variable.
limit = 2 g = (x for x in [1, 2, 3, 4] if x > limit) limit = 3 print(list(g)) factor = 10 g2 = (x * factor for x in [1, 2, 3]) factor = 100 print(list(g2)) fs = list(lambda: i for i in range(3)) print([f() for f in fs]) fs = [lambda i=i: i for i in range(3)] print([f() for f in fs])
The filter used 3, not 2; the lambdas share one i until a default argument pins it
[4] [100, 200, 300] [2, 2, 2] [0, 1, 2]
Only the outermost iterable is fixed at creation. If a threshold, a factor or any other closed-over name changes before you consume the genexp, the results change with it. To pin a value, consume the generator before the change, or bind it as a default argument (lambda i=i: i) or inside a helper function.
List comprehension versus generator expression
| List comprehension | Generator expression | |
|---|---|---|
| Evaluation | Eager: all items built at once | Lazy: one item per next() |
| Memory | O(n) | O(1) |
| Reuse | Many passes | One-shot, then empty |
| Indexing | result[3] works | No indexing |
len() | Works | Not available |
The choice follows from how the data is used. Take a list comprehension when you must walk the result twice, index into it, or ask for its length, or when the data is small enough that memory does not matter. Take a genexp when you make a single pass over data that is large or unbounded.
Feeding Consumers, the Anti-Pattern and Scope
Consumers that take a genexp directly
The built-ins that reduce an iterable accept a genexp directly, so no intermediate list is needed. These are sum, any, all, min and max. The same goes for str.join, set and dict. any and all also stop pulling items as soon as the answer is known.
words = ['apple', 'kiwi', 'fig'] print(sum(len(w) for w in words)) print(any(w.startswith('k') for w in words)) print(all(len(w) > 2 for w in words)) print(min(len(w) for w in words)) print(max(len(w) for w in words)) print('-'.join(w.upper() for w in words)) print(set(len(w) for w in words)) print(dict((w, len(w)) for w in words))
12 True True 3 5 APPLE-KIWI-FIG {3, 4, 5} {'apple': 5, 'kiwi': 4, 'fig': 3}
The anti-pattern: a list you never needed
Writing sum([x*x for x in it]) builds the entire list just to add it up and throw it away. That is real wasted memory, and the cost grows with the input. ''.join([str(x) for x in it]) is a milder case. str.join needs a sized sequence, so it converts any iterable to a list internally anyway, and the explicit brackets cost almost nothing extra. Dropping the brackets there is still the cleaner style. The saving that matters is on sum, min, max, any and all, which stream.
import tracemalloc def peak(fn): tracemalloc.start() fn() _, top = tracemalloc.get_traced_memory() tracemalloc.stop() return top big = range(200_000) with_list = peak(lambda: sum([x * x for x in big])) with_gen = peak(lambda: sum(x * x for x in big)) print(with_list > 1_000_000) print(with_gen < 50_000) print(sum([x * x for x in big]) == sum(x * x for x in big))
Same answer, very different peak memory
True True True
sum([...]), min([...]), max([...]), any([...]) and all([...]) allocate a full list for nothing. any([...]) is worse, because the list is built completely before the first test, so you lose the early exit. Drop the brackets.
Comprehensions have their own scope
In Python 3, every comprehension and genexp runs in its own function-like scope. The loop variable lives only inside it and never leaks into the enclosing function, which Python 2 list comprehensions did. Your own variable with the same name is left alone. The one deliberate exception is an assignment expression (:=), which binds in the enclosing scope. The outermost iterable is also evaluated in the enclosing scope, which ties back to why it is evaluated early.
x = 'outer' squares = [x * 2 for x in range(3)] print(x) total = list(i for i in range(3)) try: print(i) except NameError: print('i is not defined')
outer i is not defined
Use parentheses when the data is walked once and may be large. Use brackets when you need a real list. Whichever you pick, remember that only the outermost iterable is fixed at creation.
Part 9 · Two-Way Communication: send, throw, close
yield as an expression and send()
So far yield has only pushed values out. It is also an expression, so it can produce a value. In received = yield value, the generator hands value to the caller, pauses, and later resumes with received set to whatever the caller passed in. This turns a one-way stream into a conversation.
The caller supplies that value with g.send(x). It resumes the generator and makes the paused yield evaluate to x. The generator runs until its next yield, and send returns the value yielded there, just as next() would. In fact next(g) is the same as g.send(None).
- 1Caller calls g.send(x)control moves into the generator
- 2Paused yield evaluates to xreceived = x
- 3Generator runs onuntil it reaches the next yield
- 4New yield value comes backit becomes the return value of send()
def echo(): received = yield 'ready' while True: received = yield f'got {received}' g = echo() print(next(g)) print(g.send('a')) print(g.send('b'))
The first next() runs to the first yield; each send() answers the previous yield
ready got a got b
Priming: the first call must be empty
A brand-new generator has not run any code yet, so no yield is paused and waiting for a value. You must therefore advance it once with next(g) or g.send(None) to reach the first yield. This step is called priming. Sending a real value to an un-primed generator raises TypeError: can't send non-None value to a just-started generator.
The classic example is an accumulator. The line total += yield total yields the running total, then adds whatever the caller sends in. The example below also shows the priming error.
def acc(): total = 0 while True: total += yield total a = acc() try: a.send(10) except TypeError as e: print(e) print(next(a)) print(a.send(10)) print(a.send(5))
The first send(10) fails; next(a) primes the generator
can't send non-None value to a just-started generator 0 10 15
Calling g.send(value) on a fresh generator raises TypeError. Always call next(g) first, or use g.send(None). The value that next returns is the generator's first yield, so do not discard it if it matters.
throw() and close(): injecting exceptions
send pushes a value into the paused yield. throw pushes an exception in instead. g.throw(ValueError) raises the exception at the exact point where the generator is suspended. If the generator wraps that yield in try/except, it can handle the error and carry on. It then runs to its next yield, and throw returns that yielded value. If the generator does not catch the exception, it propagates out of throw to the caller and the generator is finished.
def tolerant(): total = 0 while True: try: total += yield total except ValueError: print('bad value, resetting') total = 0 a = tolerant() next(a) print(a.send(5)) print(a.throw(ValueError)) print(a.send(2))
The generator catches the thrown error and keeps running
5 bad value, resetting 0 2
g.close() is the shutdown call. It raises a special exception, GeneratorExit, at the paused yield. The generator should run any cleanup it needs and then let the exception propagate, either by not catching it or by re-raising it. If the generator yields another value after catching GeneratorExit, Python raises RuntimeError. After a clean close the generator is finished.
from inspect import getgeneratorstate def worker(): print('open') try: while True: yield except GeneratorExit: print('cleanup') raise w = worker() next(w) w.close() print(getgeneratorstate(w))
Cleanup runs, GeneratorExit propagates, the generator ends closed
open cleanup GEN_CLOSED
| Method | What the paused yield does | What the call returns |
|---|---|---|
| next(g) | evaluates to None | the next yielded value |
| g.send(x) | evaluates to x | the next yielded value |
| g.throw(Exc) | raises Exc at that point | the next yielded value if handled; otherwise Exc reaches the caller |
| g.close() | raises GeneratorExit | None; the generator is finished |
Do not yield inside an except GeneratorExit block, and do not catch it and carry on. Python raises RuntimeError: generator ignored GeneratorExit. Put cleanup in finally or re-raise.
Classic coroutines and how async def differs
With send, throw and close in place, a generator is no longer just a producer. It is a function that can be suspended, fed data, interrupted and shut down. These generators are the classic coroutines of PEP 342, and they are the ancestor of async def and await.
The two are not the same thing, though. A generator-based coroutine is driven by next() and send() calls that you write. An async def function returns a native coroutine object, which uses its own protocol built on __await__. You do not step it with next(); you await it, and an event loop such as asyncio drives it. The coroutine object does have send, throw and close methods, but it has no __next__.
| Generator coroutine (PEP 342) | async def coroutine | |
|---|---|---|
| Created by | def with yield | async def |
| Driven by | your own next() and send() calls | an event loop, through await |
| Protocol | iterator protocol (next) | awaitable protocol (await) |
| Can you call next() on it? | yes | no |
import asyncio async def job(): return 1 c = job() print(hasattr(c, '__await__'), hasattr(c, '__next__')) c.close() print(asyncio.run(job()))
A native coroutine is awaitable, not iterable
True False 1
The @coroutine priming decorator
Forgetting to prime is such a common bug that people wrote a small decorator to remove it. The decorator creates the generator, calls next() once so it is already waiting at its first yield, and returns it ready for send. Older coroutine code used this idiom everywhere.
from functools import wraps def coroutine(func): @wraps(func) def start(*args, **kwargs): g = func(*args, **kwargs) next(g) return g return start @coroutine def running_total(): total = 0 while True: total += yield total r = running_total() print(r.send(10)) print(r.send(5))
No manual priming: the decorator already advanced to the first yield
10 15
send answers a paused yield, throw makes it raise, and close makes it raise GeneratorExit for cleanup. Prime a fresh generator first, either by hand or with a decorator. Modern asynchronous code uses async def, which is a separate protocol.
Part 10 · Delegation with yield from
Handing the loop to a subiterator
Sooner or later a generator needs to hand part of its job to another iterable. The obvious way is a loop that re-yields every item. yield from iterable does the same thing in one statement: it pulls each item out of the sub-iterable and yields it to your caller, in order, until the sub-iterable is exhausted. Then your generator carries on with the next line.
def manual(sub): for x in sub: yield x def delegated(sub): yield from sub def chain(*its): for it in its: yield from it print(list(manual('ab')), list(delegated('ab'))) print(list(chain([1, 2], (3,), range(4, 6))))
The first two are equivalent; the third is a one-line itertools.chain.
['a', 'b'] ['a', 'b'] [1, 2, 3, 4, 5]
chain shows how far the idea stretches. Each argument can be a different kind of iterable, because yield from only needs something it can call iter() on. The real itertools.chain is written in C and is the one to use in production. Writing your own is a good way to see what delegation does.
Why it exists: a transparent tunnel
If replacing a four-line loop were all yield from did, PEP 380 would hardly have been needed. The real reason is that it also forwards send(), throw() and close() to the subgenerator. While the subgenerator is running, the delegating generator is out of the picture. The caller talks straight through it. The hand-written loop cannot do this, because for x in sub: yield x throws away every value sent in and cannot pass an exception down. Getting it right by hand takes a page of try/except code.
- 1Callercalls next(), send(v), throw(e) or close()
- 2Outer generatorsuspended on its yield from line, only a pipe
- 3Subgeneratorreceives the call at its own paused yield
The subgenerator can also finish with return value. A plain for loop never sees that value, because it is hidden in StopIteration.value. The expression form result = yield from subgen() catches that exception for you and gives you the value as the result of the expression. The example below uses all of this. A running total lives in the inner generator. Values sent by the caller go straight to it. An exception thrown from outside ends the inner generator, and its total comes back to the outer one.
def inner(): total = 0 try: while True: total += yield total except ValueError: return total def outer(): result = yield from inner() print('inner returned', result) yield 'done' g = outer() print(next(g)) print(g.send(5)) print(g.send(10)) print(g.throw(ValueError))
send() and throw() go to inner() even though the caller only holds outer().
0 5 15 inner returned 15 done
Notice that outer contains no code for receiving values or catching ValueError. The caller sends numbers to outer, but inner is the one that receives them. When throw(ValueError) arrives, it is raised inside inner, which handles it and returns 15. That value becomes result in outer, which then goes on to yield 'done'. The same forwarding applies to close(): closing outer closes inner first.
for x in subgen(): yield x silently drops the subgenerator's return value, and it also drops anything sent in. If you need either one, you need yield from. Using yield from on a plain list or tuple is fine, but the result of the expression is then just None.
Recursive traversal and the string trap
Recursion is where yield from is most natural. To flatten arbitrarily nested data, you walk the items. When an item is itself a container, you delegate to a recursive call of the same generator. Otherwise you yield the item as it is. Each level of nesting hands its work to the level below, and the caller sees one flat stream.
from collections.abc import Iterable def flatten(items): for item in items: if isinstance(item, Iterable) and not isinstance(item, (str, bytes)): yield from flatten(item) else: yield item print(list(flatten([1, [2, [3, 'ab']], (b'xy', 4)])))
The isinstance guard is the important line.
[1, 2, 3, 'ab', b'xy', 4]
The guard clause and what it protects against
The isinstance(item, Iterable) and not isinstance(item, (str, bytes)) condition looks fussy, but it is the whole trick. A string is iterable, and iterating over it gives one-character strings. A one-character string is also iterable, and iterating over it gives itself. So a flatten without the guard recurses forever. This is the classic infinite-recursion bug of the flatten exercise. Python stops it with a RecursionError. The guard also keeps bytes whole. Bytes would not recurse forever, because their items are ints, but they would still be split into numbers when you almost certainly wanted them kept as one value.
def bad_flatten(items): for item in items: try: iter(item) except TypeError: yield item else: yield from bad_flatten(item) try: list(bad_flatten([1, 'ab'])) except RecursionError: print('RecursionError: a one-character string never stops iterating')
No string guard: 'ab' becomes 'a', and 'a' yields 'a' again, forever.
RecursionError: a one-character string never stops iterating
hasattr(item, '__iter__') or isinstance(item, Iterable) on its own is not enough, because every string passes it. Whenever you recurse over data you did not build yourself, ask what the smallest element looks like, and make sure that element stops the recursion.
What delegation costs, and where it stopped being the future
Delegation is not free, but it is cheaper than the loop it replaces. With for x in sub: yield x, every item is received by your generator's bytecode and then yielded again, which means running Python-level instructions once per item. With yield from, the interpreter drives the sub-iterator itself and passes each item straight to your caller. The per-item loop is gone, so it is measurably faster on long streams. The exact gain depends on your Python version and on how cheap the items are, so run timeit on your own data. Do not rely on a number from a blog post.
| Manual re-yield | yield from | |
|---|---|---|
| Python-level loop | One iteration per item in your generator | None, the interpreter drives the sub-iterator |
| Speed on long streams | Slower | Measurably faster |
| send / throw / close | Lost unless hand-coded | Forwarded to the subgenerator |
| Subgenerator's return value | Dropped | Result of the expression |
Depth still costs frames
Saving the loop does not remove the stack. Every level of delegation is a live generator frame, and a recursive flatten of nested data holds one frame per level of nesting. Python's recursion limit counts these frames, so very deep nesting fails even though each level is small. The limit applies to depth only. Flattening a very wide list is fine.
import sys def nest(depth): obj = 1 for _ in range(depth): obj = [obj] return obj print(sys.getrecursionlimit()) print(list(flatten(nest(100)))) try: list(flatten(nest(5000))) except RecursionError: print('too deep')
Reuses flatten() from the previous page. The limit shown is Python's default.
1000 [1] too deep
If the nesting depth is controlled by outside input, do not flatten it with recursion. Use an explicit stack: a list of iterators, where you push a nested container and pop an iterator when it runs out. That uses heap memory instead of frames, so the recursion limit stops mattering.
The await of its day
Before asyncio had async def and await, coroutines were written as generators, and yield from was how one coroutine waited on another. The @asyncio.coroutine decorator marked such a generator, and yield from other_coro() suspended it until the result was ready. That is the same delegation idea you have just seen: a pipe to a subgenerator, plus its return value. Native coroutines replaced this style, and @asyncio.coroutine was removed in Python 3.11.
| Generator-based coroutine | Native coroutine | |
|---|---|---|
| Defined with | def plus yield from | async def plus await |
| Waiting on another coroutine | yield from other() | await other() |
| Status today | Legacy, decorator removed in 3.11 | The standard |
| Use it for | Plain generator delegation, as in this section | All asyncio code |
Use yield from to delegate to iterables and subgenerators: chaining, recursive traversal, splitting a generator into parts. Use await for anything that waits on I/O. They share a mechanism, but they are separate tools, and mixing them in new code is only confusing.
Part 11 · The itertools Toolbox
Infinite sources and terminating combinators
The itertools module is a box of small, lazy building blocks. Each one takes iterables and returns an iterator, so nothing is computed until something pulls a value. The three infinite sources never stop on their own. Whatever consumes them has to supply the stopping rule.
| Source | What it yields | Bounded by |
|---|---|---|
count(start, step) | start, start+step, start+2*step, ... forever | islice, takewhile or a break |
cycle(iterable) | the items over and over, in order, forever | islice, takewhile or a break |
repeat(obj, times) | the same object; endless if times is omitted | the times argument itself |
The terminating combinators reshape or trim a stream and end when their input ends. Together they cover most of the small loops you would otherwise write by hand.
| Tool | What it does |
|---|---|
chain(a, b, ...) | walks each iterable in turn as one stream |
chain.from_iterable(its) | same, but takes one iterable of iterables, so it can be lazy too |
islice(it, stop) or islice(it, start, stop, step) | slicing for any iterator; consumes the skipped items |
takewhile(pred, it) | yields while the predicate holds, then stops for good |
dropwhile(pred, it) | skips while the predicate holds, then yields everything that remains |
filterfalse(pred, it) | keeps the items for which the predicate is false |
compress(data, selectors) | keeps the items whose matching selector is truthy |
accumulate(it) | running totals; the full story comes later in this section |
The example below runs every one of them on a small list. Notice that takewhile and dropwhile split the list at the first even number (6), and that dropwhile stops testing after that point, so the trailing 7, 9 and 2 all pass through.
from itertools import count, cycle, repeat, islice, chain, takewhile, dropwhile, filterfalse, compress print(list(islice(count(10, 5), 4))) print(list(islice(cycle('ab'), 5))) print(list(repeat('x', 3))) nums = [1, 3, 5, 6, 7, 9, 2] odd = lambda n: n % 2 print(list(chain([1, 2], 'ab'))) print(list(chain.from_iterable([[1, 2], [3], []]))) print(list(takewhile(odd, nums))) print(list(dropwhile(odd, nums))) print(list(filterfalse(odd, nums))) print(list(compress('abcde', [1, 0, 1, 0, 1]))) print(list(islice(nums, 2, 5)))
Infinite sources bounded, then the terminating tools
[10, 15, 20, 25] ['a', 'b', 'a', 'b', 'a'] ['x', 'x', 'x'] [1, 2, 'a', 'b'] [1, 2, 3] [1, 3, 5] [6, 7, 9, 2] [6, 2] ['a', 'c', 'e'] [5, 6, 7]
list(count()), list(cycle(x)) and list(repeat(x)) never return and eventually exhaust memory. Always put an islice, a takewhile or a break between an infinite source and anything that collects it.
groupby: consecutive runs only
groupby(iterable, key) walks the input once and starts a new group every time the key changes. It does not gather equal keys from across the whole input. It only merges neighbours. So the same key can appear in several groups unless the data is already ordered by that key.
- 1sortorder the data by the key function
- 2groupbypass the very same key function
- 3list(group)copy each group before moving on
The last step matters because of how groupby is built. It produces one shared stream and hands out each group as a window onto it. Asking the outer iterator for the next group moves that shared stream forward, which invalidates the previous group's sub-iterator. An old group then looks empty, because its items were skipped over. Calling list() on a group while you are still inside its loop turn is how you keep its contents.
from itertools import groupby words = ['apple', 'avocado', 'banana', 'blueberry', 'apricot'] first = lambda w: w[0] for k, g in groupby(words, first): print(k, list(g)) print('--') for k, g in groupby(sorted(words, key=first), first): print(k, list(g)) print('--') it = groupby([1, 1, 2, 2]) k1, g1 = next(it) k2, g2 = next(it) print(k1, list(g1)) print(k2, list(g2)) print([(k, list(g)) for k, g in groupby([1, 1, 2, 2, 3])])
Fragments, the sorted fix, and an invalidated group
a ['apple', 'avocado'] b ['banana', 'blueberry'] a ['apricot'] -- a ['apple', 'avocado', 'apricot'] b ['banana', 'blueberry'] -- 1 [] 2 [2, 2] [(1, [1, 1]), (2, [2, 2]), (3, [3])]
The first loop shows the fragment problem: 'a' appears twice because 'apricot' is not next to the other a-words. Sorting by the same key fixes it. In the third part, advancing to the second group made g1 empty, while the one-line comprehension is safe because it copies each group before the outer iterator moves on.
Forgetting to sort gives fragmented groups with no error. Storing the group objects themselves, as in [g for k, g in groupby(data)], gives you invalidated iterators. Sort with the same key, and store list(g).
Combinatorics and tee
Four functions generate arrangements of their input, and they differ only in whether order matters and whether an item can be reused. All of them are lazy, so you can pull the first few results from an enormous space without building it.
| Function | Order matters? | Repeats allowed? | Count for n items, size r |
|---|---|---|---|
product(a, b, ...) | yes | yes, across inputs | product of the input lengths (repeat=r gives n to the power r) |
permutations(it, r) | yes | no | n! / (n-r)! |
combinations(it, r) | no | no | n! / (r! (n-r)!) |
combinations_with_replacement(it, r) | no | yes | (n+r-1)! / (r! (n-1)!) |
Laziness does not save you from the size of the answer. Counts grow factorially, so walking the whole space is only realistic for small inputs. math.perm and math.comb compute the count without generating anything. Use them to check before you loop.
tee(iterable, n) solves a different problem: you have one iterator but need several independent readers. It returns n iterators fed from the original. To make that work, tee buffers every item that the fastest reader has taken but the slowest has not. Memory therefore grows with the gap between your slowest and fastest consumer.
from itertools import product, permutations, combinations, combinations_with_replacement, tee from math import perm, comb print(list(product('ab', [0, 1]))) print(list(permutations('abc', 2))) print(list(combinations('abc', 2))) print(list(combinations_with_replacement('ab', 2))) print(perm(10, 10), comb(50, 5), perm(20, 20)) a, b = tee(iter(range(5))) print(next(a), next(a), next(a)) print(list(b))
The four generators, their sizes, and a tee with a gap
[('a', 0), ('a', 1), ('b', 0), ('b', 1)] [('a', 'b'), ('a', 'c'), ('b', 'a'), ('b', 'c'), ('c', 'a'), ('c', 'b')] [('a', 'b'), ('a', 'c'), ('b', 'c')] [('a', 'a'), ('a', 'b'), ('b', 'b')] 3628800 2118760 2432902008176640000 0 1 2 [0, 1, 2, 3, 4]
Ten items fully permuted already give 3,628,800 results, and twenty give about 2.4 quintillion. In the tee part, a ran three items ahead, so those three sat in the buffer until b caught up. If one reader will consume everything before the other starts, list() is simpler and no worse on memory.
Once you have called tee(it), keep using only the returned iterators. Advancing the original it directly takes items that the tee copies never see.
zip, batches, windows and running folds
In Python 3, zip() is lazy: it pulls one item from each input per step and builds a tuple. It stops as soon as the shortest input runs out. That is usually what you want, but it can silently drop data. itertools.zip_longest(..., fillvalue=...) keeps going until the longest input ends and fills the gaps with a value you choose.
The same machinery gives the classic batching idiom, zip(*[iter(x)] * n). The list multiplication repeats one iterator object n times, so all n slots of each tuple pull from the same cursor and each tuple takes the next n items. Its weakness is that an incomplete final batch is dropped.
from itertools import zip_longest nums = [1, 2, 3] letters = 'ab' print(list(zip(nums, letters))) print(list(zip_longest(nums, letters, fillvalue='-'))) print(list(zip(*[iter(range(6))] * 3))) print(list(zip(*[iter(range(7))] * 3)))
Shortest, longest, and the batching idiom
[(1, 'a'), (2, 'b')] [(1, 'a'), (2, 'b'), (3, '-')] [(0, 1, 2), (3, 4, 5)] [(0, 1, 2), (3, 4, 5)]
The last line shows the loss: with seven items, the 6 vanished. Python 3.12 adds itertools.batched(iterable, n), which reads clearly and keeps a short final batch. On 3.11 you can write the same thing yourself with islice, as below. Next to it, pairwise(it) (3.10+) yields overlapping consecutive pairs, which is exactly what deltas and sliding windows of size two need.
from itertools import islice, pairwise def batched(iterable, n): it = iter(iterable) while batch := tuple(islice(it, n)): yield batch print(list(batched(range(7), 3))) temps = [20, 23, 22, 27] print([b - a for a, b in pairwise(temps)]) print(list(pairwise('abcd')))
A batched stand-in for 3.11, and pairwise for deltas
[(0, 1, 2), (3, 4, 5), (6,)] [3, -1, 5] [('a', 'b'), ('b', 'c'), ('c', 'd')]
accumulate(data, func, initial=...) is a lazy running fold. With no function it adds. With any two-argument function it carries the result forward and yields each intermediate value. If you pass initial, that value is yielded first and the output is one item longer than the input.
import operator from itertools import accumulate data = [3, 1, 4, 1, 5] print(list(accumulate(data))) print(list(accumulate(data, max))) print(list(accumulate(data, operator.mul, initial=1))) print(list(accumulate(data, max, initial=0)))
Running sum, running maximum, running product
[3, 4, 8, 9, 14] [3, 3, 4, 4, 5] [1, 3, 3, 12, 12, 60] [0, 3, 3, 4, 4, 5]
| Hand-written loop | itertools name |
|---|---|
| manual index arithmetic for slices of a stream | islice |
| nested loops over several lists | product |
prev variable to compare neighbours | pairwise |
| running total kept in a variable | accumulate |
| slicing a list in steps of n | batched or the zip idiom |
Every primitive in itertools is implemented in C. They avoid the per-item cost of Python bytecode and hold only a tiny amount of state, so they usually beat the equivalent Python loop on both speed and memory.
If your loop has an itertools name, reach for it. The C version is already written, tested and faster, and it composes with the rest of the toolbox.
Part 12 · Performance, Memory & Complexity Trade-offs
Space, time and latency
Laziness is a trade, not a free upgrade. A generator gives up some raw speed and some convenience, and in return it gives you a constant memory footprint and an instant first result. Before reaching for one, you should know exactly what you are buying and what it costs.
Space: the headline reason to go lazy
A list comprehension builds every element and holds them all at once, so its memory grows as O(n). A generator keeps one suspended frame and one current item, so its memory is O(1) however long the stream is. Ten million squares in a list cost hundreds of megabytes; the same ten million as a genexp cost a few hundred bytes. This is the single biggest reason to go lazy.
| List comprehension | Generator / genexp | |
|---|---|---|
| Space | O(n), every item held | O(1), one frame plus one item |
| Time to first item | O(n), must finish the whole build | O(1), only the first item is computed |
| Total time for one full pass | O(n) | O(n), plus a frame resume per item |
| Second pass | free | impossible, it is exhausted |
Time: same big-O, different constant
Walking every item costs O(n) either way. The difference is the constant factor. A list comprehension runs one tight loop that appends to a list. A generator must suspend and resume its frame for every item it hands out, and that resume is real work. In a tight numeric loop expect the generator to be roughly 10-30% slower than the equivalent list comprehension. You pay that per-item overhead to avoid the O(n) allocation.
Latency: when does the first item appear?
A list comprehension is all-or-nothing: nothing is visible until the last element has been computed. A generator hands over the first item after doing only the work for that item. The program below counts how many times the expensive function ran before the first result was available. It also uses sys.getsizeof to show why that function is misleading for generators: it measures the generator object's header, not the data it will eventually produce, so a generator over ten items and one over a billion report the same size.
import sys calls = 0 def square(x): global calls calls += 1 return x * x eager = [square(x) for x in range(1000)] print('list comp calls before first item:', calls) calls = 0 lazy = (square(x) for x in range(1000)) next(lazy) print('genexp calls before first item:', calls) def squares_list(n): return [x * x for x in range(n)] def squares_gen(n): return (x * x for x in range(n)) print(sys.getsizeof(squares_gen(10)) == sys.getsizeof(squares_gen(10**9))) print(sys.getsizeof(squares_list(10**5)) > 800_000)
Latency counted in calls, size compared without printing machine-specific byte counts
list comp calls before first item: 1000 genexp calls before first item: 1 True True
sys.getsizeof(g) on a generator is small and constant because it reports only the object header. It says nothing about the memory the pending data would need if you materialized it. Use it to see that a generator holds no data, never to estimate a pipeline's total memory.
Pipelines and honest measurement
Chained genexps fuse into one pass
When you stack generator stages, no stage builds a collection. The final consumer asks the stage below for one item, which asks the stage below that, and so on down to the source. Each item travels through every stage before the next item is even touched, so the whole chain behaves like a single loop with no intermediate lists and no repeated allocation.
The trace stages below print as each item passes through. If the stages were eager, you would see all the read lines first, then all the double lines. Instead the lines interleave, which proves that one item completes the whole journey before the next one starts.
def trace(tag, it): for x in it: print(tag, x) yield x nums = trace('read', range(3)) doubled = trace('double', (x * 2 for x in nums)) total = sum(x + 1 for x in doubled) print(total)
read 0 double 0 read 1 double 2 read 2 double 4 9
Below a few thousand items, the list usually wins
Memory savings only matter when memory is actually a problem. For small inputs the list is both faster, because it avoids the per-item resume, and simpler, because you can index it, measure it and loop over it twice. The crossover depends on the item size and the work per item, but as a rule of thumb a genexp earns its keep only past a few thousand items, or when the data is unbounded, or when you will stop early.
Benchmark honestly
Intuition about generator speed is frequently wrong. It varies with the Python version, the cost of the work per item, and the size of the data. Use timeit on sizes that resemble your real workload, repeat the measurement and compare the best runs. A benchmark on ten items tells you almost nothing about ten million.
python -m timeit -s "n = 100_000" "sum([x * x for x in range(n)])" python -m timeit -s "n = 100_000" "sum(x * x for x in range(n))" python -m timeit -s "n = 100" "sum([x * x for x in range(n)])" python -m timeit -s "n = 100" "sum(x * x for x in range(n))"
Run the same pair at a small and a large n and compare. Your numbers will differ from anyone else's, and that is the point.
Timing a generator against a list on 100 items and concluding that generators are slow (or fast) is the classic error. Measure at the sizes you will really run, and measure memory separately from time, because they pull in opposite directions.
What laziness cannot do
A generator can only move forward, one item at a time, and it forgets what it has already produced. Anything that needs to look backward, look ahead, or know the total size forces you to pay for a real collection anyway.
Random access and length
There is no g[5] and no len(g). You can reach a later item with itertools.islice, but it works by consuming and discarding everything before it, so it costs O(n) and it is destructive: the items it skipped are gone. Repeating it does not start over, it continues from wherever the generator now stands. Counting with sum(1 for _ in g) is also O(n) and uses the generator up, leaving nothing for the real work.
from itertools import islice g = (x * x for x in range(10)) print(next(islice(g, 3, None))) print(next(islice(g, 3, None))) g = (x for x in range(5)) print(sum(1 for _ in g)) print(sum(1 for _ in g)) data = (n for n in [3, 1, 2]) print(sorted(data))
The same index gives a different answer the second time because the first call consumed items
9 49 5 0 [1, 2, 3]
Calling sum(1 for _ in g) to learn the length leaves g empty, as the second count of 0 above shows. If you need both the count and the items, build a list once and call len() on it.
Operations that force materialization
| Operation | Why it needs everything | Does laziness help? |
|---|---|---|
sorted(g) | the smallest item may be the last one produced | no, it builds a full list internally |
reversed(g) | needs the end first, and generators have no end access | no, it raises TypeError; wrap in list() first |
| Indexing or slicing repeatedly | each access would consume or rescan | no, use a list |
| Two or more passes | a generator cannot rewind | no, store the data or recreate the source |
any, all, min, sum, next | one forward pass is enough | yes |
The rule behind the table is simple: if the algorithm walks the data once, in order, stay lazy. If it needs the whole data set in hand at some point, laziness buys nothing there, and building the list explicitly is clearer.
Where laziness dominates: streaming I/O
The strongest case for generators is data that is large or never ends: a multi-gigabyte log file, a socket, a message queue, a paginated API. There the cost of a frame resume is invisible next to the cost of waiting for the disk or the network, and the alternative, loading everything first, may be impossible. A lazy pipeline holds only the current record, so memory stays constant no matter how much data flows through.
The example below processes an endless stream of records and keeps a running maximum. Nothing ever holds the whole stream, and islice bounds how much we look at. The same shape works with a file object (iterating a file yields one line at a time) or a network reader; just replace ticks() with the real source.
from itertools import islice def ticks(): n = 0 while True: n += 1 yield f'{n},{n % 7}' def running_max(lines): best = 0 for line in lines: _, v = line.split(',') best = max(best, int(v)) yield best print(list(islice(running_max(ticks()), 10)))
An endless source, constant state, results available immediately
[1, 2, 3, 4, 5, 6, 6, 6, 6, 6]
Go lazy for I/O, unbounded data and big single-pass work, where O(1) memory and an instant first result matter. Stay with a list for small data, anything reused, indexed, sorted or counted. Whatever is left is a measurement, so run timeit at realistic sizes instead of guessing.
Part 13 · Common Mistakes & Gotchas
Silent Exhaustion and the Truthy Trap
Most iterator bugs in production share one trait: nothing crashes. The code runs, returns a plausible value, and the value is wrong. The cause is almost always that a generator is a single-use cursor, not a container. Once it has produced its last item, it stays empty for good, and asking it again is not an error.
Iterating twice gives nothing, quietly
An exhausted generator raises StopIteration on every next() call. A for loop or list() treats that as the normal end of the stream, so a second pass over the same generator simply does zero iterations. No exception, no warning. The classic form of this bug is calling list(g) to look at the data and then sum(g) to total it. The sum receives an empty stream and returns 0, which is a perfectly valid sum.
g = (x * x for x in range(4)) print(list(g)) print(sum(g)) print(bool(g))
The second pass sees an already-finished generator.
[0, 1, 4, 9] 0 True
The last line is a second trap. The generator is spent, yet bool(g) is still True. A generator object defines neither __bool__ nor __len__, so Python falls back to the default: every object is truthy. That means if g: never tests whether the generator will yield anything, before or after it is consumed.
if g: is always true for a generator, so the else branch is dead code. To test for emptiness, pull one item with a sentinel, such as first = next(g, None), and then chain that item back in front of the rest if you still need it.
| You wrote | What you get | Why |
|---|---|---|
list(g) then sum(g) | 0 | The first call used up every item |
for x in g twice | Second loop body never runs | iter(g) returns g itself, which is already finished |
if g: | Always the true branch | No __bool__ or __len__, so the default truthiness applies |
len(g) | TypeError | A generator does not know its own length |
Fixing it
Decide whether the data is walked once or many times. If it is walked more than once, materialize it with data = list(g) and reuse the list. If it is too big for that, wrap the generator expression in a function and call the function each time you need a fresh pass. itertools.tee also works, but it buffers whatever the faster copy has read ahead, so it only helps when the copies stay close together.
Ask whether each item is needed exactly once, in order. If so, keep the stream lazy and consume it in one place. If you catch yourself writing a second loop over the same name, it should be a list.
When Evaluation Happens Later Than You Think
Laziness defers work, and deferred work reads the world at the moment it finally runs, not the moment you wrote it. Two rules govern a generator expression. The outermost iterable is evaluated immediately, when the genexp is created. Everything else, including the element expression, any filters and any inner loops, runs on demand, one step at a time.
The eager part means the genexp holds an iterator over the original object. If you rebind the name (src = []) the genexp keeps the old list, because it never looks up src again. If you mutate the same list (src.append(4)), the iterator walks the live list and will see the new item. A bad outermost expression also fails at creation, not at first use, which is the one place a genexp fails early. Every other name used inside the expression is looked up late, at consumption time.
src = [1, 2, 3] g = (x * 10 for x in src) src.append(4) print(list(g)) factor = 10 h = (x * factor for x in [1, 2]) factor = 100 print(list(h)) fs = list(lambda: i for i in range(3)) print([f() for f in fs]) fs = [lambda i=i: i for i in range(3)] print([f() for f in fs])
Mutation after creation, a late-read name, and late-binding lambdas.
[10, 20, 30, 40] [100, 200] [2, 2, 2] [0, 1, 2]
The first result shows the eager outermost iterable at work: the genexp captured the list object, so the appended 4 shows up. The second shows the lazy half: factor was 10 when the genexp was built but 100 when it ran. The last pair is the same effect in its famous form. Each lambda closes over the variable i, not over its value at that moment. By the time you call them, the loop is over and i is 2, so every lambda returns 2. A default argument is evaluated when the lambda is defined, so lambda i=i: i freezes the current value into each function.
| Part of the genexp | Evaluated | Surprise if you change it later |
|---|---|---|
Outermost iterable (for x in ___) | At creation | Rebinding the name is ignored; mutating the same object is seen |
| Element expression | Per item | Closed-over names have their value at consume time |
if filter | Per item | Same: changed globals alter the result |
| Nested inner iterables | Per outer item | Re-evaluated each time, so they can differ |
Building callbacks in a loop or comprehension with lambda: i gives you callbacks that all return the last i. Bind the value with a default argument (lambda i=i: i) or with functools.partial. The same rule applies to def inside a loop.
A lazy genexp is not a copy of its data. If the source can change before you consume the genexp, take a snapshot first with list(src) or consume the stream before changing anything.
Traps Inside the Generator Body
StopIteration cannot escape a generator
Since Python 3.7 (PEP 479), a StopIteration that is raised inside a generator body and not caught there is converted into RuntimeError: generator raised StopIteration. Before this change, the exception leaked out and the consumer's loop treated it as a normal end, so a bug could silently truncate the output. Now the bug is loud.
The practical consequence is about the bare built-in next(). It is tempting to pull items from a second iterator inside a generator and let exhaustion end things naturally. That no longer works. Give next a sentinel default and decide explicitly what to do when the data runs out, or wrap the call in try/except StopIteration and return. The example below pairs up items and shows an odd-length input breaking the first version.
def pairs(it): inner = iter(it) while True: a = next(inner) b = next(inner) yield a, b _end = object() def pairs_ok(it): inner = iter(it) while True: a = next(inner, _end) if a is _end: return b = next(inner, _end) if b is _end: return yield a, b try: print(list(pairs([1, 2, 3]))) except RuntimeError as e: print('RuntimeError:', e) print(list(pairs_ok([1, 2, 3])))
The first version fails on the odd leftover item; the second ends cleanly.
RuntimeError: generator raised StopIteration [(1, 2)]
Never rely on a bare next(inner) raising StopIteration to terminate your own generator. Use next(inner, sentinel) and return, or catch StopIteration explicitly. A plain return is the only correct way to end a generator from inside.
send() needs a paused yield, and return does not yield
Two more body-level surprises are about what a generator can accept and what it gives back. First, send(value) works by making the currently paused yield expression evaluate to value. A generator that has not started has no paused yield, so sending anything other than None raises TypeError. Advance it once with next(g) (priming it) before you send. Second, return x inside a generator does not produce x as an item. It finishes the generator, and x is stored on the StopIteration exception as its value attribute, where a for loop never looks.
def acc(): total = 0 while True: total += yield total a = acc() try: a.send(5) except TypeError as e: print('TypeError:', e) next(a) print(a.send(5), a.send(7)) def count_to(n): for i in range(n): yield i return 'done' print(list(count_to(2))) g2 = count_to(1) next(g2) try: next(g2) except StopIteration as e: print('return value:', e.value)
Priming a generator, and finding the returned value.
TypeError: can't send non-None value to a just-started generator 5 12 [0, 1] return value: done
The list contains only the yielded 0 and 1; the string 'done' appears nowhere in it. To read a return value, either catch StopIteration as above or let another generator collect it with result = yield from count_to(2).
If a consumer loop is missing its last value, check for a return value in the generator. Use yield value for data that the loop should see, and keep return for the final status that only a yield from caller reads.
Mutation, Cleanup and Debugging
Changing a container while looping over it
An iterator over a container keeps a position, and that position is meaningless if the container changes shape underneath it. A dict and a set notice: they remember their size when iteration starts, and the next step raises RuntimeError: dictionary changed size during iteration. A list does not notice. The iterator just keeps an index, so removing an element shifts everything left and the next element is jumped over, with no error. Iterate over a copy, such as list(d), or build a new collection with a comprehension.
d = {'a': 1, 'b': 2}
try:
for k in d:
d[k + 'x'] = 0
except RuntimeError as e:
print(e)
d = {'a': 1, 'b': 2}
for k in list(d):
d[k + 'x'] = 0
print(sorted(d))
nums = [1, 2, 2, 3]
for n in nums:
if n == 2:
nums.remove(n)
print(nums)A loud failure for the dict, a silent one for the list.
dictionary changed size during iteration ['a', 'ax', 'b', 'bx'] [1, 2, 3]
The list result is wrong: the second 2 was skipped and survived the removal. The fix is nums = [n for n in nums if n != 2], which builds a new list instead of editing the one being walked.
Abandoned generators and open resources
A generator that opens a file or a connection holds it until its frame is closed. That happens when the generator finishes, when close() is called, or when it is garbage collected. If a consumer stops early, say with break, an exception or islice, the generator is left paused with the resource still open. In CPython, dropping the last reference usually closes it right away, but a reference cycle, a stored traceback or another interpreter can delay that indefinitely. Put the resource in a with or try/finally inside the generator, then make sure the consumer closes it with contextlib.closing.
from contextlib import closing def lines(): print('open') try: yield 'a' yield 'b' finally: print('closed') with closing(lines()) as g: print(next(g)) print('after with')
Leaving the with block calls g.close(), which runs the finally.
open
a
closed
after withRelying on a paused generator being collected to close a file works by luck on CPython and fails elsewhere. If the generator owns a resource, whoever stops early owns the close() call.
Debugging a paused generator
A suspended generator is not on any call stack. The call that created it returned long ago, so when a bad value finally blows up, the traceback shows the consumer's line that pulled the item, not the code that built the pipeline or fed it bad data. Errors seem to appear far from their cause. Three things help: inspect the paused frame, validate arguments eagerly in an ordinary wrapper function that returns the generator, and use getgeneratorstate to see whether you are looking at a created, suspended or closed generator.
| Question | Tool |
|---|---|
| Is it started, paused or finished? | inspect.getgeneratorstate(g) gives GEN_CREATED, GEN_SUSPENDED, GEN_RUNNING or GEN_CLOSED |
| What are its local variables right now? | g.gi_frame.f_locals (the frame is None once the generator is closed) |
| Is it waiting on a sub-generator? | g.gi_yieldfrom |
| Why did the error appear here? | Check where the generator was created; the failing line only shows who consumed it |
All twelve at a glance
| Gotcha | Symptom | Fix |
|---|---|---|
| Reusing an exhausted generator | Empty second pass, no error | Materialize with list, or rebuild the generator |
list(g) then sum(g) | Silent 0 | Consume once, or reuse the list |
| Eager outermost iterable | Rebinding ignored, mutation seen | Snapshot the source or consume before mutating |
| Late-binding closures | All lambdas return the last value | lambda i=i: i |
StopIteration inside a body | RuntimeError: generator raised StopIteration | Use return; never bare next(inner) |
Bare next(inner) to stop | Same RuntimeError | next(inner, sentinel) or try/except |
Un-primed send() | TypeError | Call next(g) first |
| Mutating while iterating | RuntimeError for dicts, skips for lists | Iterate a copy or build a new collection |
| Abandoned generator | File or socket stays open | with inside, contextlib.closing outside |
return x | x never yielded | Use yield, or read StopIteration.value |
| Paused generator in a traceback | Error appears at the consumer | getgeneratorstate, gi_frame.f_locals, eager argument checks |
if g: | Always true | next(g, sentinel) |
Part 14 · Comparison Matrix: Choosing the Right Tool
Six tools side by side
Python gives you six ways to produce a stream of values, and they overlap a lot. They differ on a few properties: when the work happens, how much memory it holds, whether you can walk the result twice, and whether you can ask for len or an index. The table lays them out so you can pick by property instead of by habit.
| Tool | Evaluation | Memory | Second pass | Index / len | Reach for it when |
|---|---|---|---|---|---|
| List comprehension | eager | O(n) | yes | both work | small or medium data you reuse |
| Generator expression | lazy | O(1) | no, one-shot | neither | a single pass over large data |
| Generator function | lazy | O(1) | no, one-shot | neither | the logic needs more than one expression |
| Iterator class | lazy | O(1) plus your own state | only if you add a reset | only what you add | the iterator is a real domain object |
| itertools primitive | lazy | O(1) | no, one-shot | neither | a standard combinator already names the operation |
range | lazy | O(1) | yes | both work | numeric loop bounds |
A list comprehension builds the whole result immediately. That costs O(n) memory, but you get a real list: you can loop over it again, index it, sort it and call len on it. A generator expression has almost the same syntax with parentheses, but it computes one item per request and holds one frame, so memory stays flat. The price is that it can only be walked once, and it has no index and no len.
A generator function is the step up when one expression no longer fits. It can branch, keep local state, wrap work in try/finally so cleanup runs, and receive values through send. An iterator class goes further: it is lazy and stateful like the generator, but it is an ordinary object. You can give it extra methods, inspect its fields, and add a reset.
An itertools primitive such as chain, islice or accumulate is lazy, written in C, and built to be composed with other iterators. Finally, range looks lazy but behaves like a sequence. It holds O(1) memory, yet you can reuse it, index it and call len on it. It only produces integers.
r = range(0, 100, 7) print(len(r), r[3]) print(list(range(3)), list(range(3)))
range is lazy but still indexable, sized and reusable
15 21 [0, 1, 2] [0, 1, 2]
Treating range like a generator. It is not one-shot: list(r) twice gives the same answer, and r[3] works. A generator expression over the same numbers would fail on both counts.
What survives a second loop
The most practical row in the matrix is reuse. Lists, ranges and other containers produce a fresh iterator every time a for starts, so they survive any number of passes. Generators and generator expressions are themselves iterators. Once they are exhausted they stay exhausted, and a second loop quietly sees nothing.
| Object | Second for loop sees | Why |
|---|---|---|
| list, tuple, dict, set | the same items | __iter__ returns a new iterator each time |
range | the same items | a lazy sequence that builds a new iterator each time |
| generator expression | nothing | the generator is its own iterator and is spent |
| generator function result | nothing | same: one frame, walked once |
| itertools result | nothing | itertools objects are iterators |
nums = [3, 1, 2] doubled = (n * 2 for n in nums) print(list(doubled)) print(list(doubled))
The second pass raises no error, it just returns nothing
[6, 2, 4] []
Passing a generator to code that loops twice, for example computing sum(g) and then max(g). The second call sees an empty stream and returns a wrong or empty answer with no exception.
Choosing: four decision rules
Four rules cover nearly every case. Apply them in order, because the first one that fires usually settles the question. Rule 1 is about how you will use the data: if you must iterate more than once, index into it, or sort it, materialize it into a list. Sorting and reversing need every item in hand anyway, so laziness cannot help there.
Rule 2 is about the size and source of the data. If it is unbounded, huge, or streamed from I/O such as a log file or a socket, use a generator so memory stays constant. Rule 3 then chooses the form. One expression means a generator expression. Branching, state or cleanup means a generator function. A rich object with its own methods means an iterator class. Rule 4 overrides hand-rolling: if the operation already has an itertools name, use it.
Ask whether the data is walked once. If yes, stay lazy. If no, pay the O(n) memory and take the list. When the size is small, a list is also simpler and often faster.
The same rules in code
Rule 4 is the one people forget. A named combinator is already written in C, already tested, and composes with the rest of your pipeline. accumulate is a running fold, chain joins iterables end to end, and groupby collects consecutive items that share a key. Notice that groupby only groups neighbours, so sort by the same key first if the data is not already ordered.
from itertools import accumulate, chain, groupby print(list(accumulate([3, 1, 4, 1, 5]))) print(list(chain('a', 'bc'))) rows = ['apple', 'avocado', 'banana', 'blueberry', 'cherry'] print({k: list(g) for k, g in groupby(rows, key=lambda s: s[0])})
The group key is the first letter, so the last key is 'c'
[3, 4, 8, 9, 14] ['a', 'b', 'c'] {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
When no combinator fits and the logic has state and cleanup, write a generator function. This one keeps a running total, accepts values through send, and reports when it is closed. A try/finally inside a generator is something a generator expression cannot express.
def running_total(): total = 0 try: while True: total += yield total finally: print('closed') g = running_total() next(g) print(g.send(5)) print(g.send(7)) g.close()
Prime with next(), send values, then close to trigger cleanup
5 12 closed
Finally, when the iterator deserves to be an object in its own right, make it a class. It can carry extra methods that a generator cannot, such as a reset.
class Countdown: def __init__(self, n): self.start = n self.n = n def __iter__(self): return self def __next__(self): if self.n <= 0: raise StopIteration self.n -= 1 return self.n + 1 def reset(self): self.n = self.start c = Countdown(3) print(list(c)) c.reset() print(list(c))
A domain object: lazy, stateful, and resettable
[3, 2, 1] [3, 2, 1]
Lazy for I/O and large data, eager for small data reused more than once, and an itertools name before any hand-written loop. Everything else is a measurement, not an opinion.
Part 15 · Summary & Field Cheat Sheet
The Model in One Page
Everything in this chapter hangs off three words. An iterable is anything that can hand you a fresh iterator. An iterator is the object that actually walks, and it can only go forward once. A generator is an iterator you did not have to write, because the compiler built it from a function containing yield.
| Term | Must have | Reusable? | Who writes it |
|---|---|---|---|
| Iterable | __iter__ | Yes, each call gives a new iterator | You, or the container |
| Iterator | __iter__ (returns self) and __next__ | No, exhausted for good | You, by hand |
| Generator | Both, supplied automatically | No, one-shot | The compiler, from a yield |
The for statement is only sugar over that protocol. It asks the iterable for an iterator once, calls next() over and over, and treats StopIteration as the quiet signal to leave the loop. Unpacking, list(), sum(), in and every comprehension drive the same machinery.
Here is the same loop written out by hand. The try/except is exactly what the interpreter does for you.
def desugared(iterable): it = iter(iterable) while True: try: x = next(it) except StopIteration: break print(x) desugared('ab')
a b
A generator function adds one more rule. yield freezes the frame, with its locals and its position, and hands a value out. next() thaws the frame right after that yield. A return closes the frame for good, and its value is not yielded. It rides out inside StopIteration.value.
def gen(): print('a') yield 1 print('b') return 'done' g = gen() print(next(g)) try: next(g) except StopIteration as e: print('value:', e.value)
Calling gen() runs nothing; the first next() runs up to the yield.
a
1
b
value: doneTwo-Way Generators and Lazy vs Eager
yield is an expression, so a value can travel back in. send(x) makes the paused yield evaluate to x and then runs to the next yield. throw(exc) raises an exception at the pause point, where the generator may catch it. close() raises GeneratorExit there, so cleanup in finally runs. A brand-new generator has no paused yield to receive a value, so prime it with next(g) first.
def acc(): total = 0 try: while True: try: total += yield total except ValueError: print('reset') total = 0 finally: print('closed') a = acc() next(a) print(a.send(10)) print(a.send(5)) print(a.throw(ValueError)) a.close()
10 15 reset 0 closed
yield from sub hands control to a subgenerator and forwards send, throw and close straight through to it. When the subgenerator returns, the expression yield from evaluates to its return value.
def inner(): x = yield 1 print('inner got', x) return 'r' def outer(): r = yield from inner() print('outer got', r) yield 2 g = outer() print(next(g)) print(g.send('hi'))
1 inner got hi outer got r 2
The bracket choice decides when the work happens. Both forms evaluate the outermost iterable immediately, while everything else, the filter and the expression, is evaluated later by a genexp and read from the surrounding names at consume time.
| Syntax | Evaluation | Memory | Reuse |
|---|---|---|---|
[ ... ] | Eager | O(n) | Many passes, index, len |
( ... ) | Lazy | O(1) | One-shot |
| Outermost iterable | Eager in both | Whatever it is | Captured at creation |
data = [1, 2, 3] g = (x * 2 for x in data) data = [10] print(list(g)) n = 5 h = (x + n for x in range(2)) n = 100 print(list(h))
The old list is captured, but n is looked up late.
[2, 4, 6] [100, 101]
The headline win of laziness is memory. A lazy pipeline holds one frame and one item, however long the stream is, while a list holds everything. The price is per-item overhead from resuming a frame, so a tight loop over a small list can be slightly faster than the generator.
import sys big = [x * x for x in range(100_000)] lazy = (x * x for x in range(100_000)) print(sys.getsizeof(big) > 100_000) print(sys.getsizeof(lazy) < 1000)
getsizeof(lazy) counts the object only, which is the point.
True True
| Metric | List comprehension | Generator |
|---|---|---|
| First item | O(n) | O(1) |
| Full pass | O(n) | O(n), a little slower per item |
| Memory | O(n) | O(1) |
Bugs, itertools and Pipelines
Three bugs account for most production surprises with generators. Each one is silent or nearly so, which is why they are worth memorising.
A second pass over a spent generator yields nothing and raises nothing, so sum(g) quietly returns 0. Rebuild the generator, or materialise a list if you need more than one pass.
Lambdas created in a loop read the loop variable when they are called, not when they are made, so every one sees the final value. Freeze it with a default argument such as lambda i=i: i.
Since Python 3.7 a stray StopIteration inside a generator body becomes RuntimeError. Use next(it, sentinel) or catch the exception, never a bare next(inner) meant to end the generator.
g = (x for x in range(3)) print(list(g), sum(g)) fs = [lambda: i for i in range(3)] print([f() for f in fs]) fs = [lambda i=i: i for i in range(3)] print([f() for f in fs]) def bad(it): yield next(it) yield next(it) try: print(list(bad(iter([1])))) except RuntimeError as e: print(e)
[0, 1, 2] 0 [2, 2, 2] [0, 1, 2] generator raised StopIteration
Before you write a loop, check whether itertools already names it. Those primitives are implemented in C, are already tested, and stay lazy.
| Tool | What it gives you | Watch out for |
|---|---|---|
islice | Bounded slice of any iterator | Consumes what it skips; no random access |
chain | Several iterables as one stream | Use chain.from_iterable for a list of lists |
groupby | Runs of consecutive equal keys | Sort by the same key first; groups are invalidated on advance |
accumulate | Lazy running fold, sums or maxima | Pass a function and initial= as needed |
tee | Independent copies of one iterator | Buffers the gap to the slowest copy |
batched | Fixed-size chunks | Python 3.12+ only |
from itertools import islice, chain, groupby, accumulate, tee, count print(list(islice(count(10, 5), 4))) print(list(chain([1, 2], 'ab'))) words = sorted(['pear', 'fig', 'plum', 'kiwi', 'apple'], key=len) print([(k, list(g)) for k, g in groupby(words, key=len)]) print(list(accumulate([3, 1, 4, 1, 5], max))) a, b = tee(range(3)) print(sum(a), list(b))
[10, 15, 20, 25] [1, 2, 'a', 'b'] [(3, ['fig']), (4, ['pear', 'plum', 'kiwi']), (5, ['apple'])] [3, 3, 4, 4, 5] 3 [0, 1, 2]
Stacked genexps make a constant-memory ETL pipeline. Each stage pulls exactly one item from the stage below, so the same shape works over a multi-gigabyte log file or an unbounded network stream.
import io log = io.StringIO('ok 12\nerr 7\nok 30\nok x\n') rows = (line.split() for line in log) good = (r for r in rows if r[0] == 'ok' and r[1].isdigit()) total = sum(int(r[1]) for r in good) print(total)
Swap the StringIO for open('big.log') and nothing else changes.
42Debugging and Decision Rules
When a generator misbehaves, look at where it is paused. inspect.getgeneratorstate(g) reports GEN_CREATED, GEN_RUNNING, GEN_SUSPENDED or GEN_CLOSED. While it is suspended, g.gi_frame.f_locals shows its live variables. A closed generator has no frame, so inspect it before you close it.
from inspect import getgeneratorstate def walker(items): for pos, item in enumerate(items): yield item g = walker('xyz') print(getgeneratorstate(g)) next(g) next(g) print(getgeneratorstate(g)) print(g.gi_frame.f_locals['pos']) g.close() print(getgeneratorstate(g))
GEN_CREATED
GEN_SUSPENDED
1
GEN_CLOSEDTracebacks from a paused generator show the consumer, not the definition, so errors appear where the value was pulled. The state and locals above are the quickest way back to the cause.
| Situation | Reach for | Why |
|---|---|---|
Control flow, state, cleanup, send | Generator function | Logic exceeds an expression; try/finally works |
| Needs methods, reset or pickling | Iterator class | The iterator is itself a domain object |
| A combinator already names the job | itertools | C speed, composable, tested |
| Integer loop bounds | range | Lazy, reusable, indexable, has len |
Go lazy for I/O and large data, and go eager for small data you will reuse more than once. Anything between those two is a measurement, not an opinion, so time it at realistic sizes with timeit.
Part 16 · Check yourself
Quiz
Try to answer each question in your head before you open the answer. They test whether you can predict what the interpreter does, not whether you remember the wording.
What do these two lines print, and why does the second one not raise an error?
- It prints
[0, 1, 2]and then0. list(g)drives the generator until it raisesStopIteration, so the generator is exhausted.sum(g)asks an exhausted iterator for items, getsStopIterationstraight away, and sums nothing. An empty sum is0, and no error is raised.- Fix: build a list once if you need two passes, or recreate the generator for each pass.
g = (x for x in range(3)) print(list(g)) print(sum(g))
What does this print? Pay attention to when the genexp reads its source.
- It prints
[2, 4, 6, 8]. - The outermost iterable is evaluated when the genexp is created, but that gives you a live list iterator over the same list object, not a snapshot.
- Appending before the first
next()is therefore visible. Rebindingdata = []would not have mattered, because the genexp already holds the original list. - If you need a frozen copy, pass
list(data)ortuple(data)as the source.
data = [1, 2, 3] g = (x * 2 for x in data) data.append(4) print(list(g))
Spot the bug. Why does this crash instead of ending quietly with a single item?
- It raises
RuntimeError: generator raised StopIteration. - The first
next(it)returns1. The second finds the iterator empty and raisesStopIterationinside the generator body. - Since Python 3.7 (PEP 479) that exception is converted to
RuntimeErrorinstead of silently ending the generator. - Fix: use
next(it, sentinel)andreturnwhen you get the sentinel, or catchStopIterationand return.
def first_two(it): yield next(it) yield next(it) print(list(first_two(iter([1]))))
What do the three calls give, and what changes if you delete the next(a) line?
- It prints
10and then15. next(a)primes the generator: it runs to the firstyieldand hands out0.a.send(10)makes the pausedyieldevaluate to10, sototalbecomes10. The loop reachesyield totalagain and sends10back to the caller.send(5)gives15the same way.- Without
next(a), the firstsend(10)raisesTypeError: can't send non-None value to a just-started generator, because there is no pausedyieldto receive the value.
def acc(): total = 0 while True: total += yield total a = acc() next(a) print(a.send(10)) print(a.send(5))
Spot the bug. This flatten works on [1, [2, 3]] but blows up on [1, 'ab']. Why?
- A string is iterable, and each item you get from it is a one-character string, which is also iterable.
- So
flatten('a')yields fromflatten('a')forever, and you end withRecursionError. - Fix: treat text as a leaf, with
isinstance(x, Iterable) and not isinstance(x, (str, bytes))as the test for descending. - Related trap:
yield fromon a string by itself is fine and yields characters. The bug is the recursion on those characters.
def flatten(items): for x in items: try: iter(x) except TypeError: yield x else: yield from flatten(x)
Summary
- An iterable has
__iter__, an iterator has__iter__and__next__, and a generator is an iterator the compiler wrote for you. foris sugar foriter(), repeatednext(), and stopping onStopIteration. Unpacking,list(),sum()and comprehensions use the same protocol.yieldfreezes a frame andnext()thaws it.returncloses the generator and parks its value inStopIteration.value.send,throwandclosemake a generator a two-way coroutine, prime it withnext()first, andyield fromforwards all three to a subgenerator.- A genexp is lazy and one-shot, a list comprehension is eager and reusable, and in both the outermost iterable is evaluated immediately.
- Lazy code costs O(1) memory and gives a fast first result, but you pay per-item overhead, and you lose
len, indexing and a second pass. - The classic bugs are reusing an exhausted generator, late-binding closures, and
StopIterationleaking out of a generator body (PEP 479). - Name it before you build it: reach for
itertoolssuch asislice,chain,groupby,accumulateandbatchedbefore writing your own loop.