Skip to content

Async Views in Django 3.1

Learn about new Django async features like async views, middleware, and tests.


Django 3.1. finally supports async views, middleware, and tests. This article gives an overview of the new asynchronous features, why it’s important, and how you can use it with little effort to speed up your application. Along the way we build a small demo application demonstrating how to implement the async functionalities.

Why Async?

Asych IO is a concurrent programming design that enables „pausing“ a function while waiting on the result and letting other functions (routines) run in the meantime. With this you can achieve a more efficient use of resources compared to a synchronous approach. It is often a perfect fit for IO-bound tasks such as

  • Reading from/writing to files
  • Making calls to external APIs or websites and waiting for the response
  • Interacting with a database
  • Calling other microservices to perform background tasks, e.g., to send emails

So if you build applications that have to deal with a high number of tasks simultaneously, then your app might benefit a lot from an asynchronous code. Let’s have a look at one such example in action!

Creating The Project

In this tutorial we build a simple Django application that lets us plan a roundtrip through five different cities. In order to see if our destinations are a good choice, we want to know the weather in each of these cities. In our app this means we have to make five calls to an external weather API, and it takes a significant amount of time for each request until the response is received. This is an example where we can really benefit from an asynchronous approach, because instead of making one request after the other, we can now make concurrent requests for all cities.

Let’s start by creating our project. For more information have a look at the installation and setup commands

$ mkdir django-tutorial && cd django-tutorial
$ python3 -m venv venv
$ source venv/bin/activate

(venv)$ pip install django
(venv)$ django-admin.py startproject roundtrip .

Sync and Async Views

To demonstrate the benefit of asynchronous views, let’s implement both a synchronous and an asynchronous view. Create a new file roundtrip/views.py and add two view functions like this:

from django.http import JsonResponse

def weather_sync(request):
    payload = {"message": "Hello World!"}
    return JsonResponse(payload)

async def weather_async(request):
    payload = {"message": "Hello Async World!"}
    return JsonResponse(payload)

Note that for the asynchronous view we declare the function with async def . You can learn more about the async/await syntax here.

Update the URLS in roundtrip/urls.py:

from django.urls import path

from roundtrip.views import weather_sync, weather_async

urlpatterns = [
    path("sync/", weather_sync),
    path("async/", weather_async)
]

Run The Application

Initialize Django and start the development server:

(venv)$ python manage.py migrate
(venv)$ python manage.py runserver  

In the browser we can navigate to http://127.0.0.1:8000/sync/ and to http://127.0.0.1:8000/async/ to test our to views. For both routes we should see the corresponding JSON response.

Speed Up The Application With Concurrent Code

Now we implement the requests to the external API. For this we need a HTTP client with async support. We use httpx here. Open the Terminal window and install it:

pip install httpx

Add the following code in roundtrip/views.py

import asyncio
import time
import httpx

from django.http import JsonResponse


def http_call_sync(url):
    print(f"sync call to {url}...")
    time.sleep(1)
    r = httpx.get(url)
    return r.json()


async def http_call_async(url):
    print(f"async call to {url}...")
    async with httpx.AsyncClient() as client:
        await asyncio.sleep(1)
        r = await client.get(url)
        return r.json()

In both functions we implement an HTTP GET request to the given url and return the response in JSON format. We simulate a computing time of 1 second by using the sleep(1) function. Note that in the async case we need to use asyncio.sleep(1) . To learn more about asyncio and coroutines with the async/await syntax, I recommend to read through the official docs.

Now modify the two views and add the functionality to call the weather API for different cities. As our external weather API we can use the simple Talk Python weather service:

BASE_URL = "https://weather.talkpython.fm/api/weather"
cities = ["portland", "berlin", "chicago", "madrid", "sidney"]


def weather_sync(request):
    responses = []
    for city in cities:
        res = http_call_sync(f"{BASE_URL}?city={city}")
        responses.append(res)

    result = {"responses": responses}
    return JsonResponse(result)


async def weather_async(request):
    tasks = []
    for city in cities:
        res = http_call_async(f"{BASE_URL}?city={city}")
        tasks.append(res)

    responses = await asyncio.gather(*tasks)
    result = {"responses": responses}
    return JsonResponse(result)

Go to http://127.0.0.1:8000/sync/ and test it. After around five seconds you should see the returned JSON data. Now test the async view at http://127.0.0.1:8000/async/. You should see the result much faster!

What is happening here?

The synchronous approach should be straightforward. We make one API call after the other and append the received data to the result. For each call we need to wait one second, thus taking at least five seconds in total. But what about the async function?

When applying the http_call_async , the function code is not yet executed. Instead we get back an awaitable object that we can use at a later point. asyncio.gather() is then used to run all awaitable objects concurrently. At this point we use the await keyword, meaning here we wait until we get all actual results back. In each async function we still sleep for one second. But since all functions are now running concurrently, Python can intelligently use the waiting time and execute one of the other functions in the meanwhile. As a result, we get the response much faster!

Async Tests

Now that we are able to build async views, it would be really nice to test them asynchronously, too. Since Django 3.1 this is also very simple. Create a new file roundtrip/test_views.py with the following code:

from django.test import TestCase
from django.test import AsyncClient


class TestApiWithClient(TestCase):
    async def test_api_with_async_client(self):
        client = AsyncClient()
        response = await client.get("/async/")
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertEqual(data["responses"][0]["location"]["city"], "Portland")

In order to run Django tests we need to execute python manage.py test. This will execute all tests and summarize the results. In our case, all tests should pass.

Async Middleware

Middleware is a framework of hooks into Django’s request/response processing. It’s a light „plugin“ system for globally altering the input or output. It can now support any combination of synchronous and asynchronous requests. Add a new file roundtrip/middleware.py with the following code, making use of the @sync_and_async_middleware decorator to cover both cases:

import asyncio
import json
import time

from django.http import JsonResponse
from django.utils.decorators import sync_and_async_middleware


@sync_and_async_middleware
def timing_middleware(get_response):
    if asyncio.iscoroutinefunction(get_response):
        async def middleware(request):
            start = time.perf_counter()
            response = await get_response(request)
            data = json.loads(response.content)
            data["elapsed"] = time.perf_counter() - start
            return JsonResponse(data)
    else:
        def middleware(request):
            start = time.perf_counter()
            response = get_response(request)
            data = json.loads(response.content)
            data["elapsed"] = time.perf_counter() - start
            return JsonResponse(data)

    return middleware

To take effect, you also have to add this middleware to roundtrip/settings.py.

MIDDLEWARE = [
    
    'roundtrip.middleware.timing_middleware',
]

When testing the two views in the browser, you should now see a new field in the returned JSON data containing the elapsed time for the request. Test it and find out how much faster the asynchronous view is!

ASGI Server

Until now, we’ve used the built-in Django development server. It is able to detect async views and runs them in a thread within its own event loop . But to get actual asynchronous support, we need to use an ASGI server. A popular choice is Uvicorn . Install it with:

pip install uvicorn

And from the project’s root directory we can start it with:

uvicorn roundtrip.asgi:application --reload

For more information on how to run Django with uvicorn in production have a look at the official docs.

Async Adapter Functions

The ORM and other parts of Django are not completely async capable yet. So sometimes it is necessary to adapt the calling style when calling sync code from an async context, or vice-versa. For this there are two adapter functions: async_to_sync() and sync_to_async() . Both can be used either as a wrapper or a decorator. Don’t just make a synchronous and an asynchronous call inside an async view! So for example if you have to interact with the database in an async view, you should use the sync_to_async() function:

from asgiref.sync import sync_to_async

async def async_with_sync_view(request):
    loop = asyncio.get_event_loop()
    async_function = sync_to_async(some_sync_call)
    loop.create_task(async_function())
    # ...
    return JsonResponse(data)

Conclusion

In this article we learned about the new async features in Django and how they can be implemented. For many use-cases, especially for I/O bound tasks, concurrent code can speed up the application a lot. In order to extend this simple demo application, there are some more things you can try out in your async views, e.g., sending emails and reading from/writing to files.

Resources


FREE VS Code / PyCharm Extensions I Use

✅ Write cleaner code with Sourcery, instant refactoring suggestions: Link*


Python Problem-Solving Bootcamp

🚀 Solve 42 programming puzzles over the course of 21 days: Link*

* These are affiliate link. By clicking on it you will not have any additional costs. Instead, you will support my project. Thank you! 🙏