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.

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

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

python
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}).'
                )
            })

Sources