Warning: Bocadillo is now UNMAINTAINED. Users are recommended to migrate to a supported alternative, such as Starlette or FastAPI. Please see #344 for more information.
Async crash course
New to asynchronous programming in Python? Fear not! This crash course will get you up and running with async and how it impacts building web applications.
If you're already comfortable with async, feel free to skip to the tutorial.
We'll keep this short, to the point, and focused on using async in Bocadillo. To learn more, see Resources for aspiring experts at the bottom of this page.
TL;DR
- Defining an async function: use
async def
instead ofdef
. - Calling an async function: use the
await
keyword inside an async function:value = await func()
. - CPU-bound operations: use the
starlette.concurrency.run_in_threadpool
helper. - Async libraries: check out awesome-asyncio or do your own research!
Terminology
Synchronous function
Also known as a regular function, this is just a standard Python function defined using the def
syntax:
def get_attendees():
return ["John", "Mary", "Isabella"]
Asynchronous function
Also known as a coroutine function, an asynchronous function returns a coroutine and is defined using the async def
syntax:
async def get_attendees():
return ["John", "Mary", "Isabella"]
Awaitable
An awaitable is an object which can be used in an await
expression. The term "awaitable" is really not much more than a syntax-level definition.
Coroutine
The return value of an asynchronous function is a particular kind of awaitable known as a coroutine.
Coroutines are first-class citizens in Python. There's even a built-in type for them! Here, take a look:
async def get_attendees():
return ["John", "Mary", "Isabella"]
coro = get_attendees()
print(type(coro)) # <class 'coroutine'>
Working with coroutines
Basics
Consider the following asynchronous function:
async def get_items():
print("Getting items…")
return [1, 2, 3]
Let's call it:
items = get_items()
At this point, items
is a coroutine:
print(type(items)) # <class 'coroutine'>
Have you noticed that "Getting items…" has not been printed yet? This is because get_items()
has not even run yet!
"But then", you say, "how do I run the coroutine and get its result?"
Good question. Here's an inaccurate but pragmatic answer*:
- Make sure you are in an asynchronous function, i.e. one defined with
async def
. - Use the
await
syntax.
Here's what it looks like:
async def main(): # <- example async function
items = await get_items() # <- call another async function with `await`
print(items) # [1, 2, 3]
*If you're curious to know the more accurate answer, you'll find a lot of useful information in the asyncio documentation, e.g. in Coroutines and Tasks.
"But then", you say, "how am I supposed to run main()
?!"
Well, you don't, because…
Async is the interface
As an async web framework, Bocadillo provides an asynchronous runtime and takes care of running coroutines for you. This allows you to write async/await
code without worrying about who runs it and how.
If that sounds confusing, take a look at the following "Hello, World" application:
from bocadillo import App, configure
app = App()
configure(app)
@app.route("/")
async def hello(req, res):
res.text = "Hello, world!"
This code doesn't care about how the hello
function is actually run. You don't even need to know anything about asyncio
or how it works.
You just need to use the async def
syntax on the view, and Bocadillo will do the heavy lifting to handle requests in a concurrent fashion. It's magic. ✨
Summary
To sum up, here's the gist of what you need to know when working with asynchronous functions and coroutines in Bocadillo:
- Define an asynchronous function* using
async def func(): ...
. - Call an asynchronous function* and get its result using
value = await func()
. - Bocadillo provides the asynchronous runtime so you can focus on writing async code instead of worrying about how it should run.
*A view, an error handler, a hook, an HTTP middleware callback, etc.
Common patterns
Here are a few patterns you may find useful while working with async code.
Executing CPU-bound operations
In Python, async relies on cooperative multitasking. This means that if a function puts high load on the CPU without using await
, other coroutines won't be able to run in the meantime, and you'll lose concurrency.
In a web context in particular, this means that clients need to wait for said function to terminate before they can get their request processed.
To solve this issue, you can use Starlette's run_in_threadpool
helper. It will run the function in a separate thread to ensure that the main thread (where coroutines are run) does not get blocked.
Here's an example of running an expensive CPU-bound operation (sorting random numbers) in a view using run_in_threadpool
:
Not found: /home/travis/build/bocadilloproject/bocadillo/docs/guide/snippets/work_check.py
Try it out for yourself:
- Start the server:
uvicorn app:app
- Create two new terminal sessions.
- In the first terminal, run the following to start sorting 10^7 random numbers:
curl http://localhost:8000/work/7
- In the second terminal, run the following while the first terminal is still waiting for a response:
curl http://localhost:8000/check
You should be able to get OK
responses in the second terminal even though the server is still sorting numbers. This is because the sort happens in a separate thread — concurrency is preserved! 🎉
Converting a regular function to an asynchronous function
If you're given a regular function and you need to convert it to an asynchronous function, you can just write an async wrapper:
from somelib import compute
async def compute_async(*args, **kwargs):
return compute(*args, **kwargs)
Note: if compute
is CPU-bound, wrapping it in an async
function won't magically prevent it from blocking the main thread — you need to use run_in_threadpool
as well:
from starlette.concurrency import run_in_threadpool
from somelib import compute
async def compute_async(*args, **kwargs):
return await run_in_threadpool(compute, *args, **kwargs)
This can be simplified using functools.partial
:
from functools import partial
from starlette.concurrency import run_in_threadpool
from somelib import compute
compute_async = partial(run_in_threadpool, compute)
Finding async libraries to replace synchronous ones
One of the caveats associated to async is that everything needs to be asynchronous (a.k.a. non-blocking), or you may block the main thread and lose concurrency.
For this reason, you will need to use async equivalents of your favorite libraries if they don't support async natively.
For example:
- Databases instead of SQLAlchemy.
- requests-async instead of requests.
You can check out awesome-asyncio, Libraries that work with async/await or do your own research to find async libraries. The ecosystem is ever evolving — be on the lookout!
Resources for aspiring experts
If you're curious to learn more about async in Python — the history, the implementation, the ecosystem — here are a few resources we think you'll find useful.
Talks:
- Asynchronous Python for the Complete Beginner - Miguel Grinberg, PyCon 2017.
- Async/await in Python 3.5 and why it is awesome - Yury Selivanov, EuroPython 2016.
Writings:
- Async IO in Python: A Complete Walkthrough - Real Python.
Lists:
- aio-libs - The set of asyncio-based libraries built with high quality.
- awesome-asyncio - A curated list of awesome Python asyncio frameworks, libraries, software and resources.
References:
- Official asyncio documentation - A library to write concurrent code using the async/await syntax.
- ASGI - Asynchronous Server Gateway Interface.
← Installation Tutorial →