yield suspends the frame; the generator object preserves locals between resumes
When the compiler sees yield anywhere in a function body, it stops emitting a normal function and instead emits a generator function. Calling that function does not run any of the body: it builds a generator object that wraps a suspended frame. Each next() resumes the frame exactly where it was suspended, with local variables, the instruction pointer, and the evaluation stack intact. CPython tracks this with the generator state machine: GEN_CREATED, GEN_RUNNING, GEN_SUSPENDED, and GEN_CLOSED. The frame is not copied or replayed, so side effects before a yield happen exactly once.
Locals persist because the frame object stays alive; you can inspect it with gen.gi_frame and inspect.getgeneratorstate(gen).
PEP 342 added send(), throw(), and close(), turning generators into resumable coroutine-like objects. PEP 380 added yield from for delegation.
Trade-off: generators are lazy and constant-memory, but they are single-use and you cannot call len() or index them. If you need random access, materialize with list().
Common mistake: assuming the body runs at call time. The first next() is what executes up to the first yield.
Common mistake: treating an exhausted generator as empty rather than closed; further next() calls raise StopIteration forever, and generators cannot be restarted.
Version note: PEP 525 (3.6) added async generators, PEP 342 (2.5) added send/throw/close, and generator return values via StopIteration.value arrived in PEP 380 (3.3).
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience