Async Views in Django 3.1 by Jace Medlin , August 17th, 2020

Introduction

Writing asynchronous code gives you the ability to speed up your application with little effort .

With Django 3.1 finally supporting async views, middleware, and tests, now’s a great time to get them under your belt .

This post looks at how to get started with Django’s new asynchronous views .

Objectives (…, Simplify basic background tasks with Django’s async views,…)

By the end of this post, you should be able to:

  • Write an asynchronous view in Django

  • Make a non-blocking HTTP request in a Django view

  • Simplify basic background tasks with Django’s async views

  • Use sync_to_async to make a synchronous call inside an async view

  • Explain when you should and shouldn’t use async views

You should also be able to answer the following questions:

  • What if you make a synchronous call inside an async view ?

  • What if you make a synchronous and an asynchronous call inside an async view ?

  • Is Celery still necessary with Django’s async views ?

Prerequisites

As long as you’re already familiar with Django itself, adding asynchronous functionality to non-class-based views is extremely straightforward.

Dependencies

  • Python >= 3.8

  • Django >= 3.1

  • Uvicorn

  • HTTPX

What is ASGI ?

ASGI stands for Asynchronous Server Gateway Interface .

It’s the modern, asynchronous follow-up to WSGI, providing a standard for creating asynchronous Python-based web apps.

Another thing worth mentioning is that ASGI is backwards-compatible with WSGI, making it a good excuse to switch from a WSGI server like Gunicorn or uWSGI to an ASGI server like Hypercorn , Uvicorn or Daphne even if you’re not ready to switch to writing asynchronous apps .

Creating the App

Create a new project directory along with a new Django project:

$ mkdir django-async-views && cd django-async-views
$ python3.8 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin.py startproject hello_async .

Feel free to swap out virtualenv and Pip for Poetry or Pipenv.

Django will run your async views if you’re using the built-in development server, but it won’t actually run them asynchronously , so we’ll use Uvicorn to stand up the server.

Install it:

(env)$ pip install uvicorn

To run your project with Uvicorn, you use the following command from your project’s root:

uvicorn {name of your project}.asgi:application

In our case, this would be:

(env)$ uvicorn hello_async.asgi:application

Next, let’s create our first async view

Add a new file to hold your views in the “hello_async” folder, and then add the following view:

1 # hello_async/views.py
2
3 from django.http import HttpResponse
4
5
6 async def index(request):
7     return HttpResponse("Hello, async Django!")

Creating async views in Django is as simple as creating a synchronous view – all you need to do is add the async keyword .

Update the URLs:

 1 # hello_async/urls.py
 2
 3 from django.contrib import admin
 4 from django.urls import path
 5
 6 from hello_async.views import index
 7
 8
 9 urlpatterns = [
10     path("admin/", admin.site.urls),
11     path("", index),
12 ]

Now, in a terminal in your root folder, run:

(env)$ uvicorn hello_async.asgi:application --reload

The –reload flag tells uvicorn to watch your files for changes and reload if it finds any. That was probably self-explanatory.

Open http://localhost:8000/ in your favorite web browser:

Hello, async Django!

Not the most exciting thing in the world, but, hey, it’s a start.

It’s worth noting that running this view with a Django’s built-in development server will result in exactly the same functionality and output. This is because we’re not actually doing anything asynchronous in the handler.

HTTPX (A next generation HTTP client for Python)

Introduction

It’s worth noting that async support is entirely backwards-compatible , so you can mix async and sync views, middleware, and tests.

Django will execute each in the proper execution context.

Install HTTPX

(env)$ pip install httpx

views.py

To demonstrate this, add a few new views:

 1 import asyncio
 2 from time import sleep
 3
 4 # https://github.com/encode/httpx
 5 import httpx
 6 from django.http import HttpResponse
 7 from django.http.request import HttpRequest
 8
 9
10 # helpers
11 async def http_call_async():
12     print("http_call_async")
13     try:
14         for num in range(1, 6):
15             await asyncio.sleep(1)
16             print(f"async {num=}")
17
18         async with httpx.AsyncClient() as client:
19             r = await client.get("https://httpbin.org/")
20             print(r)
21     except Exception as error:
22         print(f"{error}=")
23
24
25 def http_call_sync():
26     for num in range(1, 6):
27         sleep(1)
28         print(num)
29     r = httpx.get("https://httpbin.org/")
30     print(r)
31
32
33 # views
34 async def async_hello(request: HttpRequest) -> HttpResponse:
35     return HttpResponse("Hello, async Django!")
36
37
38 async def async_view_test_1(request: HttpRequest) -> HttpResponse:
39     loop = asyncio.get_event_loop()
40     loop.create_task(http_call_async())
41     return HttpResponse("Non-blocking HTTP request")
42
43
44 def sync_view_test_1(request: HttpRequest) -> HttpResponse:
45     http_call_sync()
46     return HttpResponse("Blocking HTTP request")

Update the URLs

 1 # hello_async/urls.py
 2
 3 from django.contrib import admin
 4 from django.urls import path
 5
 6 from hello_async.views import async_view_test_1, sync_view_test_1
 7
 8
 9 urlpatterns = [
10     path("admin/", admin.site.urls),
11     path("async/", async_view_test_1),
12     path("sync/", sync_view_test_1),
13 ]

http://localhost:8000/async/

With the server running, navigate to http://localhost:8000/async/ . You should immediately see the response:

Non-blocking HTTP request

In your terminal you should see:

INFO:     127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Here, the HTTP response is sent back before the first sleep call.

http://localhost:8000/sync/

Next, navigate to http://localhost:8000/sync/ . It should take about five seconds to get the response:

Blocking HTTP request

Turn to the terminal:

1
2
3
4
5
<Response [200 OK]>
INFO:     127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK

Here, the HTTP response is sent after the loop and the request to https://httpbin.org/ completes.

Smoking Some Meats

Now, let’s write a view that runs a simple task in the background .

Back in your project’s URLconf, create a new path at smoke_some_meats:

 1 # hello_async/urls.py
 2
 3 from django.contrib import admin
 4 from django.urls import path
 5
 6 from hello_async.views import async_view_test_1, sync_view_test_1, smoke_some_meats
 7
 8
 9 urlpatterns = [
10     path("admin/", admin.site.urls),
11     path("smoke_some_meats/", smoke_some_meats),
12 ]

async def smoke

Back in your views, create a new async function called smoke .

This function takes two parameters: a list of strings called smokables and a string called flavor.

These will default to a list of smokable meats and “Sweet Baby Ray’s”, respectively.

 1 # hello_async/views.py
 2
 3 from typing import List
 4
 5 async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> None:
 6     """ Smokes some meats and applies the Sweet Baby Ray's """
 7
 8     if smokables is None:
 9         smokables = [
10             "ribs",
11             "brisket",
12             "lemon chicken",
13             "salmon",
14             "bison sirloin",
15             "sausage",
16         ]
17
18     if (loved_smokable := smokables[0]) == "ribs":
19         loved_smokable = "meats"
20
21     for smokable in smokables:
22         print(f"Smoking some {smokable}....")
23         await asyncio.sleep(1)
24         print(f"Applying the {flavor}....")
25         await asyncio.sleep(1)
26         print(f"{smokable.capitalize()} smoked.")
27
28     print(f"Who doesn't love smoked {loved_smokable}?")

The first line of the function instantiates the default list of meats if smokables isn’t provided.

The second “if” statement then sets a variable called loved_smokable to the first object in smokables, so long as the first object isn’t “ribs.”

The for loop asynchronously applies the flavor (read: Sweet Baby Ray’s) to the smokables (read: smoked meats).

async def smoke_some_meats

Next, create the async view that uses the async smoke function :

 1 # hello_async/views.py
 2
 3 async def smoke_some_meats(request: HttpRequest) -> HttpResponse:
 4     loop = asyncio.get_event_loop()
 5     smoke_args = []
 6
 7     if to_smoke := request.GET.get("to_smoke"):
 8         # Grab smokables
 9         to_smoke = to_smoke.split(",")
10         smoke_args += [[smokable.lower().strip() for smokable in to_smoke]]
11
12         # Do some string prettification
13         if (smoke_list_len := len(to_smoke)) == 2:
14             to_smoke = " and ".join(to_smoke)
15         elif smoke_list_len > 2:
16             to_smoke[-1] = f"and {to_smoke[-1]}"
17             to_smoke = ", ".join(to_smoke)
18
19     else:
20         to_smoke = "meats"
21
22     if flavor := request.GET.get("flavor"):
23         smoke_args.append(flavor)
24
25     loop.create_task(smoke(*smoke_args))
26
27     return HttpResponse(f"Smoking some {to_smoke}....")

This view takes the optional query params:

  • to_smoke is a comma-delimited list of things to smoke,

  • and flavor is what you’re applying to them.

http://localhost:8004/smoke_some_meats?flavor=blue
http://localhost:8004/smoke_some_meats?to_smoke=banana,oranges&flavor=burned

The first thing this view does (which can’t be done in a standard sync view) is grab the event loop with asyncio.get_event_loop().

It then parses the query params, if applicable (and does some string cleanup for the final print statement).

If we don’t pass in anything to smoke, to_smoke defaults to “meats.”

Finally, a response is returned to let the user know they’re being prepared a delicious BBQ meal.

Sync and Async Calls

Q: What if you make a synchronous and an asynchronous call inside an async view ?

Warning

Don’t do this.

Synchronous and asynchronous views tend to work best for different purposes.

If you have blocking functionality in an async view, at best it’s going to be no better than just using a synchronous view .

Sync to Async

If you need to make a synchronous call inside an async view ( like to interact with the database via the Django ORM , for example), use sync_to_async either as a wrapper or a decorator.

Example:

1 # hello_async/views.py
2
3 async def async_with_sync_view(request):
4     loop = asyncio.get_event_loop()
5     async_function = sync_to_async(http_call_sync)
6     loop.create_task(async_function())
7     return HttpResponse("Non-blocking HTTP request (via sync_to_async)")

Add the import to the top:

from asgiref.sync import sync_to_async

Add the URL

 1 # hello_async/urls.py
 2
 3 from django.contrib import admin
 4 from django.urls import path
 5
 6 from hello_async.views import (
 7     index,
 8     async_view,
 9     sync_view,
10     smoke_some_meats,
11     burn_some_meats,
12     async_with_sync_view
13 )
14
15
16 urlpatterns = [
17     path("admin/", admin.site.urls),
18     path("smoke_some_meats/", smoke_some_meats),
19     path("burn_some_meats/", burn_some_meats),
20     path("sync_to_async/", async_with_sync_view),
21     path("async/", async_view),
22     path("sync/", sync_view),
23     path("", index),
24 ]

Test it out in your browser at http://localhost:8003/sync_to_async/ .

In your terminal you should see:

INFO:     127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>

Using sync_to_async, the blocking synchronous call was processed in a background thread, allowing the HTTP response to be sent back before the first sleep call.

Celery and Async Views

Q: Is Celery still necessary with Django’s async views ?

It depends.

Django’s async views offer similar functionality to a task or message queue without the complexity .

If you’re using (or are considering) Django and want to do something simple (such as send an email to a new subscriber or call an external API), async views are a great way to accomplish this quickly and easily.

If you need to perform much-heavier, long-running background processes, you’ll still want to use Celery or RQ.

It should be noted that to use async views effectively, you should only have async calls in the view .

Task queues, on the other hand, use workers on separate processes, and are therefore capable of running synchronous calls in the background, on multiple servers.

By the way, by no means must you choose between async views and a message queue, you can easily use them in tandem.

For Example: You could use an async view to send an email or make a one-off database modification, but have Celery clean out your database at a scheduled time every night or generate and send customer reports.

Conclusion

In conclusion, although this was a simple use-case, it should give you a rough idea of the possibilities that Django’s new asynchronous views open up.

Some other things to try in your async views are sending emails, calling third-party APIs, and writing to a file.

Think of those views in your code that have simple processes in them that don’t necessarily need to return anything directly to the end user those can quickly be converted to async.

For more on Django’s newfound asynchronicity, see this excellent post that covers the same topic as well as multithreading and testing.