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:
- Works with
bulk_create() - No Python overhead
- Consistent across processes
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
- Python 3.10+ required
- Logout is POST-only by default
- 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:
- Database defaults reduce Python overhead
- Generated fields reduce application logic
- Async improvements help I/O-bound views
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.