What's the difference between a coroutine and a generator?
Quick Answer
Both are built on the same underlying mechanism (a suspendable function frame), but they serve different purposes: a **generator** *produces* a sequence of values one at a time via `yield`, consumed by iteration (`for`, `next()`). A **coroutine** (`async def`) is designed to be *awaited* — it represents a unit of asynchronous work that eventually produces one result, driven by an event loop rather than a `for` loop, and uses `await` instead of `yield` to suspend.
Detailed Answer
Same suspension mechanism, different intent
def gen(): # generator: produces a SEQUENCE of values
yield 1
yield 2
async def coro(): # coroutine: produces ONE eventual result
await asyncio.sleep(1)
return 42
Both gen() and coro() return objects representing suspended
computation rather than running immediately — under the hood, CPython's
native coroutines (async def) are implemented with the same frame-
suspension machinery that powers generators (historically, asyncio was
even built directly on @types.coroutine-decorated generators before
native coroutine syntax existed).
How they're driven differs
# Generator: driven by iteration
for value in gen():
print(value)
# Coroutine: driven by the event loop, via await/asyncio.run
result = await coro() # inside another coroutine
result = asyncio.run(coro()) # or, at the top level
You can't for loop over a coroutine (it's not iterable in that sense),
and you can't await a plain generator (unless it's specifically
decorated as a generator-based coroutine, a legacy pattern superseded by
async def). Trying to iterate a coroutine directly, or await a plain
generator, raises a TypeError.
Purpose: many values vs. one eventual value
- A generator's job is to lazily produce a sequence:
yieldeach value, potentially infinitely many, consumed one at a time. - A coroutine's job is to represent a single asynchronous
operation that will eventually complete with one result (or raise) —
conceptually closer to a
Future/Promisethan to an iterator, even though it's implemented with similar suspension internals.
Async generators: a hybrid
async def async_range(n):
for i in range(n):
await asyncio.sleep(0) # yield control back to the event loop
yield i
async for i in async_range(5):
print(i)
Python also supports async generators (async def containing
yield), which combine both: they lazily produce a sequence and can
await between values, consumed with async for instead of a plain
for loop — used for streaming data over an async source (e.g., reading
paginated results from an async database driver).
Interview-ready summary: Coroutines and generators share the same
suspend/resume mechanism, but generators (yield, driven by for/next)
model lazily producing a sequence of values, while coroutines (await,
driven by the event loop) model a single asynchronous operation resolving
to one eventual result — async generators combine both when you need a
lazily-produced sequence that can also await I/O between items.