A Taste of Async
Run a Coroutine
Lesson 1 ended on a rule: a handler on an async def and await, and runs a coroutine with asyncio.run, on the server, where you can watch it happen.
async def makes a coroutine function
async def label(snapshot):
return f"version {snapshot['version']}"
The only new word is async, and it changes what a call does. Calling label({"version": 3}) does not run the body. It returns a coroutine object: the call, set up and paused before its first line, waiting for something to run it. Nothing has computed "version 3" yet. A coroutine object you decide not to run can be thrown away with its close()
What does this print?
async def label(version):
return f"version {version}"
pending = label(3)
print(type(pending).__name__)
pending.close()
The type is named coroutine. No "version 3" appears, because the body never ran.
The await keyword
Inside another async def, you run a coroutine with await:
async def shout(snapshot):
line = await label(snapshot)
return line.upper()
await label(snapshot) runs label’s coroutine to its end and gives back its shout may pause. If the thing awaited has to wait, such as a send to a slow viewer or the next snapshot, the coroutine stops at the await, the label never has to wait, so here the await simply returns. Every await is a place where control can go back to the loop, which is the rule from Lesson 1 written into the code.
await belongs only inside async def. Python refuses it anywhere else before running a single line:
def shout(snapshot):
line = await label(snapshot)
return line.upper()
The message is SyntaxError: 'await' outside async function, and at the top level of a 'await' outside function. So an ordinary def cannot
Where may you write await label(snapshot)?
asyncio.run starts the loop
Something has to run the outermost coroutine. In the standard library, that is asyncio.run:
import asyncio
print(asyncio.run(shout({"version": 3})))
It creates an event loop, runs the coroutine you give it to its end, along with everything that coroutine awaits, returns the coroutine’s return value, and closes the loop. A program usually calls it once, at the top, and everything else is awaited from there.
Calling asyncio.run while a loop is already running, for example from inside a coroutine that asyncio.run started, raises RuntimeError: asyncio.run() cannot be called from a running event loop. Inside a coroutine, await other() is the way to run another one.
That error is why no example on this page calls asyncio.run. The examples you can run inside a page on this site use a version of Python built for the browser, and it is reported to run inside the page’s own event loop, where asyncio.run would raise exactly that error. This course has not verified it, so it relies on neither answer: every asyncio.run in this chapter runs on the server, in the Run of this lesson and the next.
A coroutine that is already running under asyncio.run calls asyncio.run(other()). What happens?
Forgetting await
A coroutine that is never awaited never runs. Python notices when such a coroutine object is thrown away and prints a warning that names it:
RuntimeWarning: coroutine 'label' was never awaited
The program carries on without the result, so a missing await shows up as a
This lesson runs one coroutine at a time, each awaited in turn. asyncio can also run several coroutines side by side as tasks, wait for them together, and cancel them. Lesson 4
label is a coroutine function:
async def label(snapshot):
return f"version {snapshot['version']}"
Inside another coroutine, two_labels, you write label(first) without await, where first is {"version": 4}. What does two_labels get from that call?
Try it
This part is optional, and nothing checks it. Open async_scoreboard.py, the file this chapter’s work goes into. Write label as above, and write two_labels(first, second): start an empty list, append await label(first), append await label(second), and return the list.
Press Run. It runs async_demo.py, which calls your coroutines with asyncio.run on the server. Its Lesson 2 section first calls label(...) without running it and closes the coroutine it got, then runs both of your coroutines. A finished version, run on the site, printed:
--- Lesson 2: a coroutine runs only when something runs it
calling label(...) alone gives a coroutine and runs nothing yet
asyncio.run(label(...)) gives 'version 3'
asyncio.run(two_labels(...)) gives ['version 4', 'version 5']
--- Lesson 3: your handler with a stand-in connection
not written yet
--- Lesson 3: your handler served by websockets.asyncio
pl_ws.py, a program written for this course, serves your handler with websockets.asyncio and plays three viewers:
async_scoreboard.viewer_handler is not written yet (Lesson 3)
The Run needs the websockets library, because the supplied async_scoreboard.py and pl_ws.py import it at the top; the site’s sandbox has it. The first line comes before anything of yours runs, so it appears even if you skip this part; until you write label, the next line is not written yet. The Lesson 3 sections wait for the handler you write next.
In the next lesson, the scoreboard’s viewer handler becomes a coroutine, and the waiting it does becomes an await.