Overview

Django 5.0, released on December 4, 2023, opens a new major cycle with features that address long-standing requests. GeneratedField allows defining database-computed columns directly in models, faceted filters in the admin display object counts per filter value, and field defaults can now be computed by the database.

Form rendering is also simplified thanks to Field.template_name, which allows customizing the template for each field type without overriding the entire widget.

Key Features

GeneratedField

The new GeneratedField allows creating columns whose values are automatically computed by the database from other columns. Two modes are available: STORED (the value is physically stored) and VIRTUAL (computed on every read). This is ideal for denormalizations that must always remain consistent.

python
from django.db import models
from django.db.models import F, Value
from django.db.models.functions import Concat, Lower

class Employee(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    monthly_salary = models.DecimalField(max_digits=10, decimal_places=2)
    weekly_hours = models.PositiveIntegerField(default=35)

    # Full name computed by the database
    full_name = models.GeneratedField(
        expression=Concat('first_name', Value(' '), 'last_name'),
        output_field=models.CharField(max_length=101),
        db_persist=True,  # STORED: physically persisted
    )

    # Annual salary (12 months + 1 month bonus)
    annual_salary = models.GeneratedField(
        expression=F('monthly_salary') * 13,
        output_field=models.DecimalField(max_digits=12, decimal_places=2),
        db_persist=True,
    )

    # Normalized lowercase identifier
    identifier = models.GeneratedField(
        expression=Concat(Lower('first_name'), Value('.'), Lower('last_name')),
        output_field=models.CharField(max_length=101),
        db_persist=True,
    )

# Usage: generated fields are read-only
emp = Employee.objects.create(
    first_name='Marie', last_name='Dupont', monthly_salary=3500
)
emp.refresh_from_db()
print(emp.full_name)       # Marie Dupont
print(emp.annual_salary)   # 45500.00
print(emp.identifier)      # marie.dupont

Faceted filters in the admin

The admin interface now displays the number of objects matching each filter value, directly in the sidebar. This allows seeing the data distribution at a glance without clicking on each filter. This feature is enabled simply by setting show_facets on the ModelAdmin.

python
from django.contrib import admin

class OrderAdmin(admin.ModelAdmin):
    list_display = ['reference', 'customer', 'status', 'amount', 'date']
    list_filter = ['status', 'date', 'payment_method']

    # Enable counts on filters
    show_facets = admin.ShowFacets.ALWAYS

    # Three options available:
    # ShowFacets.ALWAYS   — always show counts
    # ShowFacets.ALLOW    — show via a URL parameter
    # ShowFacets.NEVER    — disable (default)

# Result in the sidebar:
# Status
#   Pending (23)
#   Confirmed (156)
#   Shipped (89)
#   Delivered (412)
#   Cancelled (7)

Database-computed defaults

Model fields now accept a db_default parameter to define a default value computed by the database rather than by Python. This guarantees that the default is applied even during direct SQL inserts, and allows using database functions like Now().

python
from django.db import models
from django.db.models.functions import Now, Pi

class ActivityLog(models.Model):
    action = models.CharField(max_length=200)
    user = models.CharField(max_length=100)

    # Timestamp managed by the database
    created_at = models.DateTimeField(db_default=Now())

    # Counter initialized to zero by the database
    attempts = models.IntegerField(db_default=0)

    # Default priority
    priority = models.CharField(
        max_length=10,
        db_default=Value('normal'),
    )

# The object is created without specifying default fields
entry = ActivityLog.objects.create(
    action='login', user='marie'
)
entry.refresh_from_db()
print(entry.created_at)  # 2023-12-04 10:30:00+00:00
print(entry.attempts)    # 0
print(entry.priority)    # normal

Simplified form rendering

The Field.template_name attribute allows defining a rendering template for each form field type. This simplifies rendering customization without having to override widgets or write complex templatetags. Group templates (div, table, ul) are also customizable.

python
from django import forms

class RegistrationForm(forms.Form):
    # Use a custom template for this field
    email = forms.EmailField(
        template_name='forms/fields/email_with_help.html',
    )
    password = forms.CharField(
        widget=forms.PasswordInput,
        template_name='forms/fields/password.html',
    )
    terms = forms.BooleanField(
        template_name='forms/fields/checkbox.html',
    )

    # Global form rendering with a custom template
    template_name_div = 'forms/registration_div.html'

# In the main template:
# {{ form }}  — uses template_name_div by default
# {{ form.email }}  — uses the field's template

Sources