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 it's 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. These are used for streaming data over an async source, like reading paginated results from an async database driver.

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.