Overview
Django 4.1, released on August 3, 2022, marks a major milestone in the transition toward an asynchronous Django. The ORM gains a complete async interface with methods like acreate(), aget(), and afilter(). Class-based views become async-compatible, and model validation gains new capabilities.
This release lays the groundwork for a fully asynchronous Django ecosystem, making the most common database operations accessible from async/await code.
Key Features
Async ORM interface
Django's QuerySet now provides asynchronous variants of the most commonly used methods. Each synchronous method has an equivalent prefixed with a: aget(), acreate(), acount(), aexists(), and more. You can also iterate asynchronously over a QuerySet with async for.
from django.http import JsonResponse
# Async queries in an async view
async def api_recent_articles(request):
"""Return the 10 most recent published articles as JSON."""
articles = []
async for article in (
Article.objects
.filter(published=True)
.order_by('-publication_date')[:10]
):
articles.append({
'title': article.title,
'summary': article.summary,
'date': article.publication_date.isoformat(),
})
return JsonResponse({'articles': articles})
# Async creation and retrieval
async def register_attendee(request):
"""Register an attendee for a conference."""
conference = await Conference.objects.aget(pk=request.POST['conf_id'])
attendee_count = await conference.attendees.acount()
if attendee_count >= conference.capacity:
return JsonResponse({'error': 'Full'}, status=409)
attendee, created = await Attendee.objects.aget_or_create(
email=request.POST['email'],
defaults={'name': request.POST['name']},
)
await conference.attendees.aadd(attendee)
return JsonResponse({'registered': created, 'total': attendee_count + 1})
Async class-based views
Generic class-based views (CBVs) now support asynchronous handlers. Simply define the get(), post(), etc. methods as async def coroutines. Django automatically detects whether a view is synchronous or asynchronous.
from django.views import View
from django.http import JsonResponse
import httpx
class WeatherView(View):
"""Async view that queries an external API."""
async def get(self, request, city):
async with httpx.AsyncClient() as client:
response = await client.get(
f'https://api.weather.example/v1/{city}'
)
data = response.json()
# Record the lookup in the database (async)
await Lookup.objects.acreate(
city=city,
user=request.user if request.user.is_authenticated else None,
)
return JsonResponse({
'city': city,
'temperature': data['temp'],
'conditions': data['description'],
})
# urls.py
# path('weather/<str:city>/', WeatherView.as_view(), name='weather')
Model validation improvements
Django 4.1 improves model validation by making it easier to add custom error messages in the clean() method. The constraint system also gains the violation_error_message attribute to customize the error message when a constraint is violated at the database level.
from django.db import models
from django.db.models import Q, F, CheckConstraint
from django.core.exceptions import ValidationError
class Invoice(models.Model):
reference = models.CharField(max_length=20, unique=True)
amount_excl_tax = models.DecimalField(max_digits=10, decimal_places=2)
tax_rate = models.DecimalField(max_digits=4, decimal_places=2)
amount_incl_tax = models.DecimalField(max_digits=10, decimal_places=2)
issue_date = models.DateField()
due_date = models.DateField()
class Meta:
constraints = [
CheckConstraint(
check=Q(amount_excl_tax__gt=0),
name='invoice_positive_amount',
violation_error_message=(
'The pre-tax amount must be strictly positive.'
),
),
CheckConstraint(
check=Q(due_date__gte=F('issue_date')),
name='invoice_due_after_issue',
violation_error_message=(
'The due date cannot precede the issue date.'
),
),
]
def clean(self):
"""Verify consistency between pre-tax, tax rate, and total."""
expected_total = self.amount_excl_tax * (1 + self.tax_rate / 100)
if abs(self.amount_incl_tax - expected_total) > 0.01:
raise ValidationError({
'amount_incl_tax': (
f'The total ({self.amount_incl_tax}) does not match '
f'the expected calculation ({expected_total:.2f}).'
)
})
