What does `yield` actually do?

yield turns the function into a factory for paused stack frames. Calling it runs nothing; each next() runs the body up to the next yield and freezes it there. The sequence is produced on demand, so memory is constant in the length — and it can only be walked once.

Overview

The question, and what it is testing

yield turns the function into a factory for paused stack frames. Calling it runs nothing; each next() runs the body up to the next yield and freezes it there. The sequence is produced on demand, so memory is constant in the length — and it can only be walked once.

Interviewers use this one because the answer separates people who have read the language from people who have only used it. Nothing here is obscure; all of it is observable, which is what the editor below is for.

Python semanticsConceptualEasy

Step through it

What to watch

  • Calling the function runs none of the body — the state starts at GEN_CREATED.
  • Each resume ends at a yield, which is why the frame is suspended rather than finished.
  • GEN_CLOSED is terminal: iterating again produces nothing, silently.

Say this out loud

"A function with yield returns a generator instead of running. Each next() resumes the body until the next yield, so values are produced lazily and memory stays constant however long the sequence is. The trade is that it is one-shot and has no len() - if I need to iterate twice or index it, I need a list."

What does `yield` actually do?

What does yield do, and how is a generator different from a function that returns a list?

Run it

The three states, the one-shot exhaustion, and the memory difference at a million items - printed rather than asserted.

The three states, driven by hand.

1Python
Output

One-shot is not a warning, it is silence - the second consumer sees an empty sequence and nothing complains.

2Python
Output

And what the laziness actually buys, in bytes.

3Python
Output

A generator is a paused frame

An ordinary function runs to a return and its stack frame is destroyed. A function containing yield does not run at all when you call it: you get a generator object that owns a frame which has never started.

Every next() resumes that frame, runs until the next yield, hands back the value, and freezes the frame again — local variables, instruction pointer and all. That is the whole mechanism, and it is why yield can appear in the middle of a loop and still work: the loop variable survives between resumes because the frame was never torn down.

What laziness buys, in one number

The list version of a million squares allocates a million integers and a million pointers before you read the first one. The generator version allocates one object with a frame in it, and the editor below prints both sizes: a few megabytes against about a hundred bytes.

The other half is latency. A generator can hand you its first item immediately, which matters when the sequence is being read from a file or a network, and matters absolutely when the sequence is infinite. itertools.count() is a valid generator and an impossible list.

The three things you give up

It is one-shot. Once exhausted, it stays exhausted, and a second for loop over it runs zero times without error. This is the bug people actually hit: passing a generator to two functions and finding the second one saw an empty sequence.

No len(), no indexing, no slicing. The length is not known without running it, so there is nothing to report. itertools.islice is the slicing replacement.

Exceptions surface late. A generator that will raise on its fourth item raises on the fourth next(), which may be a long way from where it was created — inside a different function, or after a with block has already closed the file it was reading.

The follow-up you should expect

"What is the difference between a generator and an iterator?" An iterator is the protocol — anything with __next__ and __iter__. A generator is the easiest way to get one, written as a function instead of a class. Every generator is an iterator; most iterators in real code are generators.

And "what does a generator expression change?" Nothing but the syntax: (x*x for x in xs) is the same object as the equivalent yield function, which is worth knowing because it means sum(x*x for x in xs) never builds the list at all.

What to say out loud

A function with yield returns a generator instead of running. Each next() resumes the body until the next yield, so values are produced lazily and memory stays constant however long the sequence is. The trade is that it is one-shot and has no len() - if I need to iterate twice or index it, I need a list.

Then stop. The commonest failure on a question like this is answering it correctly in one sentence and then talking for another minute until something wrong comes out.

What to notice while it runs

  • Calling the function runs none of the body — the state starts at GEN_CREATED.
  • Each resume ends at a yield, which is why the frame is suspended rather than finished.
  • GEN_CLOSED is terminal: iterating again produces nothing, silently.

Edge cases to raise

Volunteering these is most of what separates a correct answer from a good one.

An exception raised inside the generator surfaces at the next() that triggers it, not where the generator was created - which can be in a different function, after the with block that opened the file has already closed it. Generators that read resources should own the with, not be handed an open handle.

return inside a generator sets StopIteration.value rather than producing an item. yield from reads that value; a plain for loop discards it silently.

Closing matters when there is cleanup. Abandoning a suspended generator eventually runs its finally at collection time, which is not a schedule you control. contextlib.closing or an explicit gen.close() makes it deterministic.

The follow-ups interviewers ask

"What is the difference between a generator and an iterator?" An iterator is the protocol - anything with __iter__ and __next__. A generator is the easiest way to produce one, written as a function rather than a class. Every generator is an iterator; most iterators you meet in real code are generators.

"What does yield from do?" Delegates to another iterable, yielding everything it yields - and it forwards send and throw through to the inner generator, which is what made generator-based coroutines possible before async def existed.

"Can a generator receive values as well as produce them?" Yes: value = yield makes the yield an expression, and gen.send(x) resumes it with x as that expression's value. This is how generators were used as coroutines for a decade, and it is worth knowing precisely because async replaced it.

"How do you iterate a generator twice?" You do not. Either materialise it with list(), accepting the memory, or use itertools.tee, which buffers what the slower consumer has not read yet - so it trades the memory back in proportion to how far apart they get.

Common wrong answers

"It returns a list, just lazily." There is no list at any point. If the interviewer hears "list" they will ask what len() gives, and the answer is a TypeError.

"Generators are faster." Not reliably. Each item costs a __next__ call, so in wall-clock terms a generator is often a little slower than building the list. The win is memory and first-item latency, and saying "faster" invites a measurement you will lose.

"You can index into it." You cannot. itertools.islice is the replacement, and it still has to walk the items it skips.

Recap in one screen

  • Calling the function runs none of the body - the state starts at GEN_CREATED.
  • Each resume ends at a yield, which is why the frame is suspended rather than finished.
  • GEN_CLOSED is terminal: iterating again produces nothing, silently.
  • The one-line answer: yield turns the function into a factory for paused stack frames.
  • Worth trying: Replace list(g) with a second next(g) and watch the state stay GEN_SUSPENDED until the body actually returns.
  • Worth trying: Add len(as_gen). The TypeError is the point: the length is not knowable without running it.
  • Worth trying: Wrap the generator in itertools.tee(as_gen, 2) to get two independent iterators - and note it buffers, so it trades the memory back.

How the code works

The three states, the one-shot exhaustion, and the memory difference at a million items - printed rather than asserted.

How the code works

  1. g = countdown(3)No output from the body appears on this line. The call builds a generator and returns; the print inside the function has not run.
  2. inspect.getgeneratorstate(g)GEN_CREATED, then GEN_SUSPENDED after a resume, then GEN_CLOSED. The state is the frame's, not the value's.
  3. sum(squares)The second call returns 0. Nothing raised, nothing warned — the generator was already closed, so the loop ran zero times. This is the one-shot bug in its natural habitat.
  4. sys.getsizeof(as_gen)About a hundred bytes, and it does not depend on the million. getsizeof on the list excludes the integers it points at, so the real gap is larger still.

Change one thing

  • Replace list(g) with a second next(g) and watch the state stay GEN_SUSPENDED until the body actually returns.
  • Add len(as_gen). The TypeError is the point: the length is not knowable without running it.
  • Wrap the generator in itertools.tee(as_gen, 2) to get two independent iterators — and note it buffers, so it trades the memory back.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What does calling a function containing yield do?

  2. Why does iterating a generator a second time produce nothing?

  3. The memory advantage of a generator comes from:

  4. Which of these does a generator NOT support?

Cheat sheet

What does `yield` actually do?

yield turns the function into a factory for paused stack frames. Calling it runs nothing; each next() runs the body up to the next yield and freezes it there. The sequence is produced on demand, so memory is constant in the length — and it can only be walked once.

INTERVIEW · vizlearn.in/interview/what-does-yield-actually-do.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.