0% found this document useful (0 votes)
20 views13 pages

Async Views in Django

Uploaded by

constrictor36209
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
20 views13 pages

Async Views in Django

Uploaded by

constrictor36209
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 13

testdriven.

io /blog/django-async-views/

Async Views in Django

Writing asynchronous code gives you the ability to speed up your application with little effort. Django
versions >= 3.1 support async views, middleware, and tests. If you haven't already experimented with
async views, now's a great time to get them under your belt.

This tutorial looks at how to get started with Django's asynchronous views.

If you're interested in learning more about the power behind asynchronous code along with
the differences between threads, multiprocessing, and async in Python, check out my
Speeding Up Python with Concurrency, Parallelism, and asyncio article.

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

1. Write an async view in Django


2. Make a non-blocking HTTP request in a Django view
3. Simplify basic background tasks with Django's async views
4. Use sync_to_async to make a synchronous call inside an async view
5. Explain when you should and shouldn't use async views

You should also be able to answer the following questions:

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


2. What if you make a synchronous and an asynchronous call inside an async view?
3. 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

1. Python >= 3.8


2. Django >= 3.1
3. Uvicorn
4. HTTPX

What is ASGI?

1/13
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 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.10 -m venv env
$ source env/bin/activate

(env)$ pip install django


(env)$ django-admin startproject hello_async .

Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern
Python Environments.

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 run Django with Uvicorn.

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:

# hello_async/views.py

from django.http import HttpResponse

async def index(request):


return HttpResponse("Hello, async Django!")

2/13
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:

# hello_async/urls.py

from django.contrib import admin


from django.urls import path

from hello_async.views import index

urlpatterns = [
path("admin/", admin.site.urls),
path("", index),
]

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
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.

To demonstrate this, add a few new views:

# hello_async/views.py

import asyncio
from time import sleep

import httpx

3/13
from django.http import HttpResponse

# helpers

async def http_call_async():


for num in range(1, 6):
await asyncio.sleep(1)
print(num)
async with httpx.AsyncClient() as client:
r = await client.get("https://httpbin.org/")
print(r)

def http_call_sync():
for num in range(1, 6):
sleep(1)
print(num)
r = httpx.get("https://httpbin.org/")
print(r)

# views

async def index(request):


return HttpResponse("Hello, async Django!")

async def async_view(request):


loop = asyncio.get_event_loop()
loop.create_task(http_call_async())
return HttpResponse("Non-blocking HTTP request")

def sync_view(request):
http_call_sync()
return HttpResponse("Blocking HTTP request")

Update the URLs:

# hello_async/urls.py

from django.contrib import admin


from django.urls import path

4/13
from hello_async.views import index, async_view, sync_view

urlpatterns = [
path("admin/", admin.site.urls),
path("async/", async_view),
path("sync/", sync_view),
path("", index),
]

Install HTTPX:

(env)$ pip install httpx

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.

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.

5/13
Smoking Some Meats
To simulate more of a real-world scenario of how you'd leverage async, let's look at how to run multiple
operations asynchronously, aggregate the results, and return them back to the caller.

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

# hello_async/urls.py

from django.contrib import admin


from django.urls import path

from hello_async.views import index, async_view, sync_view,


smoke_some_meats

urlpatterns = [
path("admin/", admin.site.urls),
path("smoke_some_meats/", smoke_some_meats),
path("async/", async_view),
path("sync/", sync_view),
path("", index),
]

Back in your views, create a new async helper function called smoke. This function takes two
parameters: a list of strings called smokables and a string called flavor. These default to a list of
smokable meats and "Sweet Baby Ray's", respectively.

# hello_async/views.py

async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby


Ray's") -> List[str]:
""" Smokes some meats and applies the Sweet Baby Ray's """

for smokable in smokables:


print(f"Smoking some {smokable}...")
print(f"Applying the {flavor}...")
print(f"{smokable.capitalize()} smoked.")

return len(smokables)

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

Don't forget the import:

6/13
from typing import List

List is used for extra typing capabilities. This is not required and may be easily omitted (just
nix the : List[str] following the "smokables" parameter declaration).

Next, add two more async helpers:

async def get_smokables():


print("Getting smokeables...")

await asyncio.sleep(2)
async with httpx.AsyncClient() as client:
await client.get("https://httpbin.org/")

print("Returning smokeable")
return [
"ribs",
"brisket",
"lemon chicken",
"salmon",
"bison sirloin",
"sausage",
]

async def get_flavor():


print("Getting flavor...")

await asyncio.sleep(1)
async with httpx.AsyncClient() as client:
await client.get("https://httpbin.org/")

print("Returning flavor")
return random.choice(
[
"Sweet Baby Ray's",
"Stubb's Original",
"Famous Dave's",
]
)

Make sure to add the import:

import random

7/13
Create the async view that uses the async functions:

# hello_async/views.py

async def smoke_some_meats(request):


results = await asyncio.gather(*[get_smokables(), get_flavor()])
total = await asyncio.gather(*[smoke(results[0], results[1])])
return HttpResponse(f"Smoked {total[0]} meats with {results[1]}!")

This view calls the get_smokables and get_flavor functions concurrently. Since smoke is
dependent on the results from both get_smokables and get_flavor, we used gather to wait for
each async task to complete.

Keep in mind, that in a regular sync view, get_smokables and get_flavor would be handled
one at a time. Also, the async view will yield the execution and allow other requests to be
processed while the asynchronous tasks are processed, which allows more requests to be
handled by the same process in a particular amount of time.

Finally, a response is returned to let the user know they're delicious BBQ meal is ready.

Great. Save the file, then head back to your browser and navigate to
http://localhost:8000/smoke_some_meats/. It should take a few seconds to get the response:

Smoked 6 meats with Sweet Baby Ray's!

In your console, you should see:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

Smoking some ribs...


Applying the Stubb's Original...
Ribs smoked.
Smoking some brisket...
Applying the Stubb's Original...
Brisket smoked.
Smoking some lemon chicken...
Applying the Stubb's Original...
Lemon chicken smoked.
Smoking some salmon...
Applying the Stubb's Original...
Salmon smoked.
Smoking some bison sirloin...
Applying the Stubb's Original...

8/13
Bison sirloin smoked.
Smoking some sausage...
Applying the Stubb's Original...
Sausage smoked.
INFO: 127.0.0.1:57501 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK

Take note of the order of the following print statements:

Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable

This is asynchronicity at work: As the get_smokables function sleeps, the get_flavor function
finishes up processing.

Burnt Meats
Sync Call

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

The same thing that would happen if you called a non-async function from a non-async view.

--

To illustrate this, create a new helper function in your views.py called oversmoke:

# hello_async/views.py

def oversmoke() -> None:


""" If it's not dry, it must be uncooked """
sleep(5)
print("Who doesn't love burnt meats?")

Very straightforward: We're just synchronously waiting for five seconds.

Create the view that calls this function:

# hello_async/views.py

async def burn_some_meats(request):


oversmoke()
return HttpResponse(f"Burned some meats.")

Lastly, wire up the route in your project's URLconf:

9/13
# hello_async/urls.py

from django.contrib import admin


from django.urls import path

from hello_async.views import index, async_view, sync_view,


smoke_some_meats, burn_some_meats

urlpatterns = [
path("admin/", admin.site.urls),
path("smoke_some_meats/", smoke_some_meats),
path("burn_some_meats/", burn_some_meats),
path("async/", async_view),
path("sync/", sync_view),
path("", index),
]

Visit the route in the browser at http://localhost:8000/burn_some_meats:

Burned some meats.

Notice how it took five seconds to finally get a response back from the browser. You also should have
received the console output at the same time:

Who doesn't love burnt meats?


INFO: 127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK

It's possibly worth noting that the same thing will happen regardless of the server you're using, be it
WSGI or ASGI-based.

Sync and Async Calls

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

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:

10/13
# hello_async/views.py

async def async_with_sync_view(request):


loop = asyncio.get_event_loop()
async_function = sync_to_async(http_call_sync, thread_sensitive=False)
loop.create_task(async_function())
return HttpResponse("Non-blocking HTTP request (via sync_to_async)")

Did you notice that we set the thread_sensitive parameter to False? This means that
the synchronous function, http_call_sync, will be run in a new thread. Review the docs
for more info.

Add the import to the top:

from asgiref.sync import sync_to_async

Add the URL:

# hello_async/urls.py

from django.contrib import admin


from django.urls import path

from hello_async.views import (


index,
async_view,
sync_view,
smoke_some_meats,
burn_some_meats,
async_with_sync_view
)

urlpatterns = [
path("admin/", admin.site.urls),
path("smoke_some_meats/", smoke_some_meats),
path("burn_some_meats/", burn_some_meats),
path("sync_to_async/", async_with_sync_view),
path("async/", async_view),
path("sync/", sync_view),
path("", index),
]

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

11/13
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 (and don't care about
reliability), 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.

When to Use
For greenfield projects, if async is your thing, leverage async views and write your I/O processes in an
async way as much as possible. That said, if most of your views just need to make calls to a database
and do some basic processing before returning the data, you're not going to see much of an increase (if
any) over just sticking with sync views.

For brownfield projects, if you have little to no I/O processes stick with sync views. If you do have a
number of I/O processes, gage how easy it will be to rewrite them in an async way. Rewriting sync I/O to
async is not easy, so you'll probably want to optimize your sync I/O and views before trying to rewrite to
async. Plus, it's never a good idea to mix sync processes with your async views.

In production, be sure to use Gunicorn to manage Uvicorn in order to take advantage of both concurrency
(via Uvicorn) and parallelism (via Gunicorn workers):

12/13
gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application

Conclusion
In conclusion, although this was a simple use-case, it should give you a rough idea of the possibilities
that Django's asynchronous views open up. Some other things to try in your async views are sending
emails, calling third-party APIs, and reading from/writing to files.

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

13/13

You might also like