The yield keyword turns a regular Python function into a generator: a producer that emits items lazily, one at a time, on demand. Each yield pauses the routine and hands a single item back to the caller; the next round of iteration picks up at the exact line after that yield, with every local variable still intact. This freeze-and-resume behavior is what lets a one-liner like sum(x*x for x in range(10**9)) finish on a laptop without melting it. This piece is one of 17 short answer-first explainers in our Python Concepts Explained reference, which sits inside the broader complete Python beginner guide.

Key Takeaways

  • yield turns any function into a generator. Invoking that routine doesn't run the body; instead, you get back a generator object that advances one chunk at a time, only when something asks for the next item.
  • Each yield suspends execution and emits one item. The following iteration picks up at the line after the yield, with all local state preserved.
  • Memory cost stays essentially flat. A range(10**9) producer weighs roughly 48 bytes; the equivalent materialized sequence would demand around 8 GB of RAM (Python docs on range).
  • yield from another_gen() delegates iteration to a sub-producer. It's the cleanest way to compose pipelines.
  • Reach for yield when you process streams, huge files, infinite series, or staged pipelines. Skip it when you need random access or a known length; that's what concrete containers are for.
48 B
size of a generator
over 1 billion values
~8 GB
size of an equivalent
materialized list
1
keyword change
from list to generator
5
concrete scenarios
where yield wins

What Is a Python Generator, in 30 Seconds?

A generator is a special kind of iterator that's defined with a function plus the yield keyword. Invoking that function doesn't execute any of its body; you receive a generator object instead. Each time the producer is asked for an item, whether by next(), a for loop, or any other consumer, it runs until it strikes the next yield, hands that result over, and freezes. The next request picks up from the frozen line. When execution eventually returns, the producer raises StopIteration, which a for loop catches quietly on your behalf. Compare the two patterns side by side:
def squares_list(n):
    out = []
    for i in range(n):
        out.append(i * i)
    return out

def squares_gen(n):
    for i in range(n):
        yield i * i

# Eager: builds the whole list in memory before you touch a single value
nums = squares_list(10_000_000)

# Lazy: produces one value at a time, on demand
for sq in squares_gen(10_000_000):
    if sq > 100:
        break  # we computed maybe ten values total
squares_list allocates ten million integers before you read a single entry. squares_gen emits them on demand and halts the instant the break fires, having done almost no work.
Eager list vs lazy generator: memory profile over time Eager list vs lazy generator: peak memory over time Eager (list) peaks early, stays high 0 1 4 9 16 25 ...all in RAM, all at once Lazy (generator) one value at a time 0 t=0 t=1 t=2 t=3 t=4 t=5 ...only one value in RAM at any time Both produce the same six values; the lazy version pays for one slot of memory, not six.
Eager list vs lazy generator: identical output, very different memory profile.

How Does Yield Actually Work Behind the Scenes?

Spotting a yield anywhere inside a function body flips that routine's type at compile time. Instead of normal call-and-return bytecode, the interpreter builds a tiny state machine that suspends and resumes on demand. Three things happen on every yield: the expression to its right ships back to whoever called next(); the entire local frame (variables, source position, loop counters) gets saved; control returns to the caller, paused but not finished. The next request restores the frame, resumes execution after the yield, and the cycle repeats. When the body eventually returns, the producer raises StopIteration and joins the exhausted pile. Generators are single-use; rewinding isn't supported. To traverse the same data twice, invoke the original function again for a fresh object.Close-up of a circuit board, representing data flowing through a Python generator pipeline

A Real Example: Reading a Huge File Line by Line

This is the use case generators were practically invented for. Imagine a 10-gigabyte log file. The naive approach blows up:
# Don't do this; it tries to load 10 GB into RAM
lines = open("huge.log").read().splitlines()
for line in lines:
    process(line)
The Pythonic approach leans on a generator. File objects are already iterators that emit one line per round:
# Processes one line at a time, peak memory ~ one line
with open("huge.log") as f:
    for line in f:
        process(line)
You can chain producers together to build a pipeline that filters, transforms, and counts without ever materializing the intermediate sequences:
def lines_from(path):
    with open(path) as f:
        for line in f:
            yield line.rstrip()

def errors_only(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

def first_field(lines):
    for line in lines:
        yield line.split()[0]

# Three generators, one walk through the file, peak memory ~ one line
for ts in first_field(errors_only(lines_from("huge.log"))):
    print(ts)
Each layer pulls one row, transforms it, and passes it onward. Nothing piles up.

yield vs return: Side-by-Side

The two keywords look similar and behave nothing alike.
Aspectreturnyield
Function typeregular functiongenerator function
When does code run?when invokedwhen iterated
How many can appear?any count, only one fires per callany count, all fire across iterations
What is produced?one result, then it endsone item per request, body continues
Memory profilebuilds whole output firstbuilds nothing; emits on demand
Reusable?yes, invoke again to re-runno, exhausted after a single full pass
A routine with both yield and return is still a producer. The return becomes "I'm done"; any payload passed to it lives inside the StopIteration exception (rarely useful in everyday code, but it's how yield from propagates results).

What Does yield from Do?

yield from delegates iteration to another iterable. It's syntactic sugar that replaces the verbose pattern:
# Verbose
def chain(a, b):
    for x in a:
        yield x
    for y in b:
        yield y

# Pythonic
def chain(a, b):
    yield from a
    yield from b
The yield from form also handles send() and exceptions transparently, which matters when you write coroutines (the basis for asyncio). For everyday composition, treat it as "iterate this sub-generator fully, then continue."

When Should You Use Yield?

Reach for yield in five concrete situations:
  1. Large or infinite sequences. Files, network streams, log tails, or anything spawned by an algorithm that could in principle never stop, like Fibonacci numbers or simulation steps.
  2. Pipelines. When data flows through filters and transforms, each stage can be a producer, and only a single element exists at any given layer at any given moment.
  3. Lazy evaluation. When the consumer might bail early. Don't compute what nobody will read.
  4. Decoupled producers and consumers. Generators let you split "where does data come from" from "what do we do with it" cleanly.
  5. Memory-constrained code. Embedded targets, serverless workers with tight RAM caps, or any process where peak memory matters more than throughput.

When Should You Not Use Yield?

Three signs you want a concrete container, not a lazy producer:
  • You need random access. gen[5] doesn't work. You'd have to walk from the start each time.
  • You need to know the length up front. len(gen) doesn't work either. If you must know how many items, you've already drained the producer counting them.
  • You'll traverse the same sequence multiple times. Generators exhaust after one full pass. If two loops in your code each need the data, materialize it once as a list and walk the list twice.

Common Mistakes With Yield

Four traps that bite beginners: Mistake 1: forgetting that invoking the function doesn't run it.
def gen():
    print("running")
    yield 1

gen()        # prints nothing; you got back a generator object
list(gen())  # prints "running", returns [1]
Mistake 2: walking the same producer twice.
g = (x*x for x in range(3))
print(sum(g))   # 5
print(sum(g))   # 0; the producer is exhausted
Mistake 3: passing a producer where a sequence is expected. APIs like json.dumps or libraries that need len() won't accept generators. Wrap in list(...) at the boundary if needed. Mistake 4: building a list inside a generator body. That defeats the point. If you find yourself doing out = []; for ...: out.append(...); yield out, you've written a function that returns a list, except worse.

Frequently Asked Questions

What's the difference between a generator and an iterator in Python?

Every producer is an iterator, but not every iterator is a producer. Iterators are any object implementing __iter__ and __next__. Generators are the painless way to build one: write a function, sprinkle yield, done. Under the hood, the Python runtime synthesizes the __iter__ and __next__ boilerplate for you.

Can a generator function take arguments?

Yes, exactly the same way any regular routine does. The arguments are captured in the local frame when the producer object is born, and they stay available across every iteration. The parameters aren't re-evaluated on each yield; they're frozen alongside the rest of the local state.

Can you iterate a Python generator twice?

No. Producers are single-use. Once one raises StopIteration, it's exhausted, and asking for the next item just raises StopIteration again. To traverse the same data twice, either invoke the underlying function a second time to get a fresh producer object, or store the items in a list once and walk the list as many times as needed.

When did generators get added to Python?

PEP 255 introduced producers in Python 2.2, released in 2001. The yield from syntax for delegating to a sub-producer arrived with PEP 380 in Python 3.3, released in 2012. Both are battle-tested and stable; you won't find a supported Python release that doesn't ship them (PEP 255, PEP 380).

The Bottom Line: Yield Is a Memory Switch

The shortest answer to "what does yield do" is this: it converts a function into a stream. Once a routine uses yield, invoking it doesn't execute any code; you receive a paused state machine that runs one chunk per request. That's the memory profile change that makes Python practical for log files, network streams, and any sequence you'd rather not materialize. Master yield, and you'll write Python that chews through 10 GB inputs as confidently as 10-element lists. For the rest of the most-asked Python concept questions on the internet, browse the full Python Concepts Explained index. For more deep-dives into Python syntax, see our Python syntax tutorial for beginners. To practice generators against auto-graded drills, jump to our 30 Python coding exercises for beginners. Generators also come up regularly in junior screens; our Python interview questions guide covers the three you're most likely to see.

Run Your First Generator on a Real Track

CodeGym's Python track teaches yield, generators, and the rest of Python's idiomatic core through 800+ hands-on tasks across 62 levels. The AI validator checks every submission in seconds; the AI mentor explains what broke when you get stuck. First level free; full plan on the pricing page. Start learning Python (free) →

Learn more about our mission and terms of service. Published article was last reviewed on 2026-05-25.