Django 5.0 Released: New Features in Depth

django python

Django 5.0 dropped on December 4, 2023. Let’s explore the new features in detail.

Form Field Rendering

The most visible change: better form rendering.

New Default Behavior

class ContactForm(forms.Form):
    name = forms.CharField()
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)
<!-- Old: Manual rendering -->
{% for field in form %}
    <div class="field">
        {{ field.label_tag }}
        {{ field }}
        {{ field.errors }}
    </div>
{% endfor %}

<!-- New: Just use the field -->
{{ form.name }}  <!-- Includes wrapper, label, errors -->

Form Templates

# settings.py
FORM_RENDERER = 'django.forms.renderers.DjangoDivFormRenderer'

Override templates:

templates/
└── django/forms/
    ├── p.html        # For as_p
    ├── table.html    # For as_table
    ├── ul.html       # For as_ul
    └── div.html      # For as_div (default)

Field Group Templates

class CheckoutForm(forms.Form):
    # Shipping
    ship_name = forms.CharField()
    ship_address = forms.CharField()
    ship_city = forms.CharField()
    
    # Billing
    bill_name = forms.CharField()
    bill_address = forms.CharField()
    bill_city = forms.CharField()
    
    class Meta:
        field_groups = {
            'shipping': ['ship_name', 'ship_address', 'ship_city'],
            'billing': ['bill_name', 'bill_address', 'bill_city'],
        }
{% for group in form.get_field_groups %}
    <fieldset>
        <legend>{{ group.name|title }}</legend>
        {% for field in group %}
            {{ field }}
        {% endfor %}
    </fieldset>
{% endfor %}

Faceted Filters in Admin

from django.contrib import admin

class ArticleAdmin(admin.ModelAdmin):
    list_filter = ['status', 'category', 'author']
    
    # Show counts in filters
    show_facets = admin.ShowFacets.ALWAYS
    # Options: ALWAYS, ALLOW (only shows with ?_facets=true), NEVER

Result:

Status
├── Published (127)
├── Draft (34)
└── Pending (8)

Category
├── Technology (89)
├── Business (45)
└── Science (35)

Database-Computed Defaults

Let the database generate default values:

from django.db.models.functions import Now, Random
from django.db import models

class Order(models.Model):
    # Database generates timestamps
    created_at = models.DateTimeField(db_default=Now())
    
    # UUID generated by database
    reference = models.UUIDField(
        db_default=models.Func(function='gen_random_uuid')
    )
    
    # Numeric sequence
    sequence = models.IntegerField(db_default=1)

Benefits:

GeneratedField

Computed columns stored in the database:

class Product(models.Model):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    tax_rate = models.DecimalField(max_digits=5, decimal_places=2)
    
    # Computed and stored
    price_with_tax = models.GeneratedField(
        expression=F("price") * (1 + F("tax_rate")),
        output_field=models.DecimalField(max_digits=10, decimal_places=2),
        db_persist=True  # Stored, not computed on read
    )

The database maintains this column automatically.

Enhanced Constraint Expressions

More powerful check constraints:

from django.db.models import Q, F
from django.db.models.functions import Length

class Account(models.Model):
    username = models.CharField(max_length=50)
    email = models.EmailField()
    is_verified = models.BooleanField(default=False)
    premium_until = models.DateField(null=True)
    
    class Meta:
        constraints = [
            # Username length validation
            models.CheckConstraint(
                check=Q(username__length__gte=3),
                name='username_min_length',
            ),
            # Verified accounts must have email
            models.CheckConstraint(
                check=Q(is_verified=False) | Q(email__length__gt=0),
                name='verified_needs_email',
            ),
            # Premium date must be future
            models.CheckConstraint(
                check=Q(premium_until__isnull=True) | Q(premium_until__gt=Now()),
                name='premium_must_be_future',
            ),
        ]

Async Improvements

Async View in WSGI

# Works with regular WSGI (Gunicorn, uWSGI)
async def dashboard_view(request):
    # Fetch data concurrently
    async with aiohttp.ClientSession() as session:
        tasks = [
            fetch_analytics(session),
            fetch_notifications(session),
            fetch_user_stats(session),
        ]
        analytics, notifications, stats = await asyncio.gather(*tasks)
    
    return render(request, 'dashboard.html', {
        'analytics': analytics,
        'notifications': notifications,
        'stats': stats,
    })

Async ORM Context

async def get_recent_articles():
    # Async iteration
    async for article in Article.objects.filter(published=True)[:10]:
        yield article
    
    # Async methods
    count = await Article.objects.acount()
    exists = await Article.objects.filter(featured=True).aexists()

Security: POST-only Logout

# urls.py
from django.contrib.auth.views import LogoutView

urlpatterns = [
    path('logout/', LogoutView.as_view(), name='logout'),
]
<!-- Template: Must use POST -->
<form method="post" action="{% url 'logout' %}">
    {% csrf_token %}
    <button type="submit" class="btn">Logout</button>
</form>

GET requests to logout now redirect to LOGOUT_REDIRECT_URL without logging out.

Model Field Changes

CharField Without max_length

class Note(models.Model):
    # PostgreSQL only: unlimited varchar
    content = models.CharField()  # No max_length required

Only works with PostgreSQL.

Template Changes

New Template Filters

<!-- escapeseq: Escape each item in sequence -->
{{ my_list|escapeseq|join:", " }}

<!-- json_script improvements -->
{% json_script data "my-data" encoder="myapp.encoders.CustomEncoder" %}

Management Commands

Async-Aware Test Client

from django.test import AsyncClient

async def test_async_view():
    client = AsyncClient()
    response = await client.get('/async-endpoint/')
    assert response.status_code == 200

Migration Notes

Breaking Changes

  1. Python 3.10+ required
  2. Logout is POST-only by default
  3. Some deprecated features removed

Upgrade Command

# Check for issues
python -W error::DeprecationWarning manage.py check

# Run migrations
python manage.py migrate

# Test everything
python manage.py test

Performance

No major performance changes, but:

Final Thoughts

Django 5.0 is quality-of-life improvements rather than revolutionary changes. The form rendering updates alone are worth the upgrade.

If you’re on Django 4.2 LTS, you can wait. If you’re starting fresh, use 5.0.


Django 5.0: Polished and ready.

All posts