Skip to content

Introduction

Let's forget that we ever learned generators. Let's continue from the review of suspendables.

Suspendable Type Python Implementation Definition Control Transfer Point Keywords
Explicit Control Transfer Generators def yield, yield from
Implicit Control Transfer Coroutines async def await

Coroutines are completely independent from generators1. We need not even know about generators to learn about coroutines. Generators require the use of yield and yield from keywords. Coroutines require the use of async def and optionally the use of await keyword.

Oddly enough, you can still use yield within a coroutine. This will create an object called asynchronous generator, which is both a generator and a coroutine hybrid generator. We will study the asynchronous generator in the advanced section (LINK ME) But, using yield from within a coroutine is illegal (CITE THE PEP).

Definition

A coroutine may be defined simply by adding a async before def. The following is an example of the simplest coroutine. The use of await keyword is not needed to define a coroutine. However, it is a SyntaxError to use await outside of an async def function.

Python
async def example_coroutine_function():
    return 1

type(example_coroutine_function)
# function

The coroutine function example_coroutine_function is a suspendable and an extended function, analogous to a generator function. Calling example_coroutine_function does not execute the contents of the function. Instead, it returns a coroutine object. We need some other mechanism to execute the contents of example_coroutine_function.

Python
coro = example_coroutine_function()
print(coro)
# <coroutine object example_coroutine_function at 0x111b625c0>

type(coro)
# coroutine

Coroutine does not implement the Iterator protocol

For generators, we could use next, which is part of the Iterator protocol. Coroutine does not implement the Iterator protocol. Compare the following code with the corresponding code for a generator.

Python
from collections.abc import Iterator

isinstance(coro, Iterator)
# False

coro.__iter__
# AttributeError: 'coroutine' object has no attribute '__iter__'

coro.__next__
# AttributeError: 'coroutine' object has no attribute '__next__'

next(coro)
# TypeError: 'coroutine' object is not an iterator

Automatic drive

We cannot use next to drive coroutine objects. The coroutine equivalent to next is bit more complicated because imnplicit control transfer suspendables need an event loop. The event loop invisibly and implicitly transfers the control between coroutines. We have many choices for an event loop but we will use asyncio, which is the default event loop that comes with the python standard library.

Python
import asyncio
output = asyncio.run(example_coroutine_function())
print(output)
# 1

Manual drive

We previously saw that for generators, next(generator_object) was equivalent to generator_object.send(None). For coroutines, next doesn't work but send does. send method has the same semantics as with generators:

  • send requires one argument
  • coroutine needs to be primed before we can send a non-None value
  • send(None) works but the value is received by await (we'll study await later)
Python
example_coroutine_function().send()
# TypeError: coroutine.send() takes exactly one argument (0 given)

example_coroutine_function().send('something')
# TypeError: can't send non-None value to a just-started coroutine

# There was no `await` in the coroutine function
example_coroutine_function().send(None)  
# StopIteration: 1

Custom drive

Using a proper event loop such as the one provided by asyncio is the intended way to drive a coroutine. However, we could write our own little event loop to drive the coroutine ourselves.

Python
def drive(coroutine_object):
    while True:
        try:
            coroutine_object.send(None)
        except StopIteration as e:
            return e.value

output = drive(example_coroutine_function())
print(output)
# 1
There are two important things to note here:

  • We used a while loop to make our event loop, just like we did in our pseudocode for the improved implementation of the plane ticket example.
  • The .send method and StopIteration work for a coroutine object, just like they did for the generator object.

Finally, our event loop is very rudimentary and will not work even with slightly more complicated toy examples.

Confusing nomenclature

Like the word generator, the word coroutine is ambiguous.

Question

Does coroutine refer to example_coroutine_function or example_coroutine_function()?

The answer is the same as that for generators.

Object Type Name
example_coroutine_function Coroutine function
example_coroutine_function() Coroutine object

It's best to use the word coroutine as an adjective instead of a noun.

await is like yield

Like yield, await is a control transfer point. You can suspend control at an await. await can also accept values. There is one difference — yield allowed us to yield any arbitrary value including nothing but await only allows us to await an Awaitable object.

Python
async def print_await_print():
    print('Starting execution')
    await 1  # Can't await an int because it's not an Awaitable
    print('Ending execution')

asyncio.run(print_await_print())
# ...
# TypeError: object int can't be used in 'await' expression
Python
async def print_await_print():
    print('Starting execution')
    await  # await needs something to await
    print('Ending execution')
# SyntaxError: invalid syntax
Python
import asyncio

async def print_sleep_print():
    print('Starting execution')
    await asyncio.sleep(1)  # This is a coroutine (which is an Awaitable)
    print('Ending execution')

asyncio.run(print_sleep_print())
# Starting execution
# Ending execution
Question

What is an Awaitable object?

Quite simply, an Awaitable object is an object with an __await__ method2. A coroutine is also an Awaitable object as you can see from the definition of a Coroutine.

await is also a two-way street

Python
import asyncio

async def await_sleep():
    print('Starting execution')
    value1 = await asyncio.sleep(1, result=2)
    print(value1)
    value2 = await asyncio.sleep(1, result=3)
    print(value2)
    print('Ending execution')

# This will fail because `asyncio.sleep(1)` needs an event loop!
await_sleep().send(None)
# RuntimeError: no running event loop
Python
async def no_await(x):
    return x * x

async def await_no_await():
    print('Starting execution')
    value1 = await no_await(2)
    print(value1)
    value2 = await no_await(3)
    print(value2)
    print('Ending execution')

await_no_await().send(None)

From https://github.com/python/cpython/blob/d5d3249e8a37936d32266fa06ac20017307a1f70/Lib/_collections_abc.py#L57:

Python
## coroutine ##
async def _coro(): pass
_coro = _coro()
coroutine = type(_coro)

Footnotes


  1. We are purposefully ignoring the existence of generator-based coroutines which are defunct as of Python 3.10. There is no benefit to learning generator-based coroutines except for unnecessary confusion. 

  2. Yet again, we're ignoring the existence of defunct generator-based coroutines. This is a perfect example of how generator-based coroutines cause unnecessary confusion. Feel free to skip the rest of this footnote to avoid confusion or continue at your own risk. According to the official documentation on Awaitable, the defunct generator-based coroutines are considered awaitables even though they don't have an __await__ method. As a result, isinstance(gencoro, collections.abc.Awaitable) will return False; use inspect.isawaitable() to properly detect generator-based coroutines as awaitables. 


Last update: 2022-09-13
Created: 2022-09-13

Comments