What's the difference between a coroutine and a generator?

6 minadvancedasynciocoroutinesgenerators

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: yield each 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/Promise than 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.