Overview

Django 5.1, released on August 7, 2024, brings improvements focused on security and simplicity. The new LoginRequiredMiddleware makes authentication mandatory by default across the entire site, eliminating the risk of forgetting a @login_required decorator on a sensitive view.

Database-backed sessions are simplified, and the ORM gains query optimizations that reduce the number of joins and subqueries generated for common cases.

Key Features

LoginRequiredMiddleware

The LoginRequiredMiddleware enforces authentication on all views by default. Public views (home page, registration, legal notices) are explicitly exempted with the @login_not_required decorator. This "secure by default" approach is far more robust than the reverse approach where every view must remember to require authentication.

python
# settings.py — Add the middleware
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.LoginRequiredMiddleware',  # new
    'django.contrib.messages.middleware.MessageMiddleware',
]

# views.py — All views require authentication
# EXCEPT those explicitly marked
from django.contrib.auth.decorators import login_not_required

@login_not_required
def home_page(request):
    """Public home page."""
    return render(request, 'home.html')

@login_not_required
def registration(request):
    """Public registration form."""
    # ...

# This view is automatically protected
def dashboard(request):
    """Private dashboard — authentication required."""
    return render(request, 'dashboard.html')

# For a CBV
from django.utils.decorators import method_decorator
from django.views import View

@method_decorator(login_not_required, name='dispatch')
class LegalNoticeView(View):
    def get(self, request):
        return render(request, 'legal_notice.html')

Simplified sessions

The database session backend is simplified with a new implementation that stores data directly as JSON instead of using base64-serialized pickle format. This makes sessions easier to inspect, safer, and compatible with external analysis tools.

python
# settings.py — Using the simplified backend
SESSION_ENGINE = 'django.contrib.sessions.backends.db'

# The JSON serializer is now the default
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'

# Using sessions in a view
def add_to_cart(request, product_id):
    cart = request.session.get('cart', {})
    key = str(product_id)

    if key in cart:
        cart[key]['quantity'] += 1
    else:
        product = Product.objects.get(pk=product_id)
        cart[key] = {
            'name': product.name,
            'price': str(product.price),
            'quantity': 1,
        }

    request.session['cart'] = cart
    return redirect('cart')

# Data is stored as readable JSON in the database
# {"cart": {"42": {"name": "Keyboard", "price": "89.99", "quantity": 2}}}

ORM query optimizations

Django 5.1 optimizes SQL generation in several common cases. Unnecessary subqueries are eliminated, redundant joins are merged, and Exists() expressions are better optimized. These improvements are transparent: existing code automatically benefits from more efficient queries.

python
from django.db.models import Exists, OuterRef, Count, Q

# Before Django 5.1: could generate unnecessary subqueries
# After Django 5.1: SQL optimized automatically

# Example 1: filtering with optimized Exists()
recent_orders = Order.objects.filter(
    date__gte='2024-01-01',
    customer=OuterRef('pk'),
)
active_customers = Customer.objects.filter(
    Exists(recent_orders)
)
# Django 5.1 generates a more efficient EXISTS clause

# Example 2: annotations with optimized joins
author_stats = (
    Author.objects
    .annotate(
        article_count=Count('articles'),
        published_count=Count(
            'articles', filter=Q(articles__published=True)
        ),
    )
    .filter(published_count__gt=0)
)
# The join to 'articles' is performed only once

# Example 3: smart column selection
data = (
    Order.objects
    .select_related('customer')
    .only('reference', 'amount', 'customer__name')
)
# Django 5.1 avoids loading unnecessary columns from the join

Sources