What's New in Django 6.0 - A Complete Guide

Django 6.0 was released on December 3, 2025, marking 20 years since Django was first created. This milestone release brings several highly anticipated features that the community has been waiting for. Let’s dive into everything new.

Python Support

Django 6.0 requires Python 3.12 or higher (3.12, 3.13, or 3.14). This is a significant change from Django 5.2, which supported Python 3.10 and 3.11.

# Check your Python version
python --version

# Should be 3.12+
Python 3.12.x

Major New Features

1. Background Tasks Framework

This is the biggest addition to Django 6.0. Django now includes a built-in Tasks framework for running code outside the HTTP request-response cycle. No more mandatory Celery setup for simple background tasks!

Basic Usage

from django.tasks import task
from django.core.mail import send_mail


@task
def send_welcome_email(user_email, username):
"""Send a welcome email to new users."""
send_mail(
subject="Welcome to Our Platform!",
message=f"Hello {username}, thanks for signing up!",
from_email=None,
recipient_list=[user_email],
)


# In your view or signal handler
def register_user(request):
# ... create user logic ...

# Enqueue the task - returns immediately
send_welcome_email.enqueue(
user_email=user.email,
username=user.username,
)

return redirect("registration-success")

Task Configuration Options

from django.tasks import task


@task(
priority=10, # Higher priority tasks run first
queue_name="emails", # Route to specific queue
backend="default", # Use specific backend
)
def high_priority_notification(user_id, message):
# Critical notification logic
pass


@task(takes_context=True)
def task_with_context(context, data):
"""Access task metadata via context."""
print(f"Task ID: {context.task_id}")
print(f"Enqueued at: {context.enqueued_at}")
# Process data...

Settings Configuration

Configure backends in settings.py:

TASKS = {
"default": {
"BACKEND": "django.tasks.backends.immediate.ImmediateBackend",
"QUEUES": ["default", "emails", "reports"],
}
}

Built-in Backends:

  • ImmediateBackend - Runs tasks synchronously (development/testing)
  • DummyBackend - Records tasks without executing (testing)

For production, you’ll need a third-party backend that connects to a task runner like Redis, RabbitMQ, or a database-backed queue.

Getting Task Results

from django.tasks import task


@task
def process_report(report_id):
# Long-running process
return {"status": "completed", "rows_processed": 1000}


# Enqueue and get result handle
result = process_report.enqueue(report_id=42)

# Check status
if result.is_finished:
print(result.return_value) # {"status": "completed", ...}

2. Template Partials

Template partials allow you to define reusable fragments within a single template file. No more creating dozens of tiny include files!

Defining and Using Partials

{# templates/components.html #}

{% partialdef button %}
<button class="btn btn-primary">
{{ button_text|default:"Click me" }}
</button>
{% endpartialdef %}

{% partialdef card %}
<div class="card">
<div class="card-header">{{ title }}</div>
<div class="card-body">{{ content }}</div>
</div>
{% endpartialdef %}

{# Use the partials #}
<div class="toolbar">
{% partial button %}
{% with button_text="Save" %}
{% partial button %}
{% endwith %}
{% with button_text="Cancel" %}
{% partial button %}
{% endwith %}
</div>

Including Partials from Other Templates

The new template_name#partial_name syntax works with {% include %}, get_template(), and render():

{# templates/page.html #}

{# Include a partial from another template #}
{% include "components.html#button" with button_text="Submit" %}

{# Include multiple partials #}
{% include "components.html#card" with title="Welcome" content="Hello!" %}
# In Python code
from django.template.loader import get_template

# Load just the partial
button_template = get_template("components.html#button")
html = button_template.render({"button_text": "Download"})

Migration from django-template-partials

If you were using the third-party django-template-partials package, Django provides a migration guide in the official docs.

3. Content Security Policy (CSP) Support

Built-in CSP support makes it easier to protect your application against XSS attacks.

Basic Configuration

# settings.py
from django.utils.csp import CSP

MIDDLEWARE = [
# ... other middleware ...
"django.middleware.security.ContentSecurityPolicyMiddleware",
]

SECURE_CSP = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF, CSP.NONCE],
"style-src": [CSP.SELF, CSP.UNSAFE_INLINE],
"img-src": [CSP.SELF, "https:", "data:"],
"font-src": [CSP.SELF, "https://fonts.gstatic.com"],
"connect-src": [CSP.SELF],
}

# For report-only mode (doesn't block, just reports violations)
SECURE_CSP_REPORT_ONLY = {
"default-src": [CSP.SELF],
# ... same structure as SECURE_CSP
}

Using Nonces in Templates

Add the context processor to enable nonce support:

# settings.py
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"OPTIONS": {
"context_processors": [
# ... other processors ...
"django.template.context_processors.csp",
],
},
},
]
{# In your template #}
<script nonce="{{ csp_nonce }}">
// This script will be allowed
console.log("Hello, secure world!");
</script>

<script>
// This will be BLOCKED by CSP
</script>

CSP Constants

Django provides constants for common CSP values:

from django.utils.csp import CSP

CSP.SELF # 'self'
CSP.UNSAFE_INLINE # 'unsafe-inline'
CSP.UNSAFE_EVAL # 'unsafe-eval'
CSP.NONCE # Uses the generated nonce
CSP.NONE # 'none'
CSP.STRICT_DYNAMIC # 'strict-dynamic'

4. Modernized Email API

Django now uses Python’s modern email.message.EmailMessage class instead of the legacy MIME classes.

What’s New

from django.core.mail import EmailMessage

email = EmailMessage(
subject="Hello",
body="This is the message body.",
from_email="from@example.com",
to=["to@example.com"],
)

# The message() method now returns email.message.EmailMessage
msg = email.message() # Returns modern EmailMessage, not MIMEMultipart

# New policy argument for fine-grained control
msg = email.message(policy=email.policy.SMTP)

# Attachments now accept MIMEPart objects
from email.mime.text import MIMEText
email.attach(MIMEText("attachment content"))

Breaking Changes

If you have custom EmailMessage subclasses, review your overrides:

# These properties are REMOVED in Django 6.0:
# - mixed_subtype
# - alternative_subtype
# - encoding

# Update custom subclasses to use the new API

Other Notable Features

New Template Variables

forloop.length in For Loops

{% for item in items %}
Item {{ forloop.counter }} of {{ forloop.length }}
{% endfor %}

AsyncPaginator

New async-compatible paginator for async views:

from django.core.paginator import AsyncPaginator


async def my_view(request):
paginator = AsyncPaginator(queryset, per_page=25)
page = await paginator.aget_page(request.GET.get("page"))

async for obj in page:
# Process objects asynchronously
pass

StringAgg on All Databases

StringAgg aggregate is no longer PostgreSQL-only:

from django.db.models import Value
from django.db.models.functions import StringAgg

# Works on SQLite, MySQL, PostgreSQL, Oracle
Author.objects.annotate(
all_books=StringAgg("book__title", delimiter=", ")
)

Aggregate Ordering

Aggregates now support order_by:

from django.db.models import F
from django.db.models.functions import StringAgg

Author.objects.annotate(
books_by_year=StringAgg(
"book__title",
delimiter=", ",
order_by=F("book__published_date").asc(),
)
)

GeneratedField Refresh

Generated fields and fields with expressions are now automatically refreshed after save() on supported backends (PostgreSQL, SQLite, Oracle):

class Product(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2)
tax_rate = models.DecimalField(max_digits=4, decimal_places=2)
total = models.GeneratedField(
expression=F("price") * (1 + F("tax_rate")),
output_field=models.DecimalField(max_digits=10, decimal_places=2),
db_persist=True,
)


product = Product(price=100, tax_rate=0.2)
product.save()
print(product.total) # 120.00 - automatically refreshed!

BigAutoField Default

New projects now use BigAutoField (64-bit) by default instead of AutoField (32-bit):

# If you need the old behavior, set explicitly:
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

Admin Improvements

  • Font Awesome Free (v6.7.2) icons
  • Distinct icons for DEBUG and INFO message levels
  • New AdminSite.password_change_form for customization

GIS Enhancements

  • GEOSGeometry.hasm property for M dimension detection
  • Rotate database function
  • BaseGeometryWidget.base_layer for custom map providers
  • New lookups and functions for MariaDB 12.0.1+

Backwards Incompatible Changes

Python Version

  • Minimum Python 3.12 - Drop support for Python 3.10 and 3.11

Database Support

  • MariaDB 10.5 dropped - Minimum is now MariaDB 10.6

ORM Changes

Custom lookups and expressions must return params as tuples, not lists:

# Before (Django 5.x)
def as_sql(self, compiler, connection):
return "SQL", [param1, param2] # List

# After (Django 6.0)
def as_sql(self, compiler, connection):
return "SQL", (param1, param2) # Tuple

Email API Changes

The following are removed from EmailMessage:

  • mixed_subtype property
  • alternative_subtype property
  • encoding property

Deprecations

Email Function Arguments

All optional parameters must use keyword arguments:

# Deprecated (positional)
send_mail(subject, body, from_email, [to], False)

# Correct (keyword)
send_mail(subject, body, from_email, [to], fail_silently=False)

URL Handling

Default protocol in urlize and urlizetrunc changes from HTTP to HTTPS:

# To prepare, set in settings.py:
URLIZE_ASSUME_HTTPS = True

ADMINS/MANAGERS Format

Tuples are deprecated, use email strings:

# Deprecated
ADMINS = [("Name", "email@example.com")]

# New format
ADMINS = ["email@example.com"]

Upgrading to Django 6.0

Step-by-Step Guide

  1. Upgrade Python first:

    # Ensure Python 3.12+
    python --version
  2. Update Django:

    pip install Django==6.0
  3. Run checks:

    python manage.py check
    python manage.py makemigrations
  4. Review deprecation warnings:

    python -Wa manage.py test
  5. Update settings:

    # settings.py

    # If you need 32-bit auto fields (legacy)
    DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

    # Prepare for HTTPS default in urlize
    URLIZE_ASSUME_HTTPS = True
  6. Test thoroughly before deploying to production.

Quick Reference

Feature Import/Setting
Background Tasks from django.tasks import task
Template Partials partialdef, partial template tags
CSP Middleware django.middleware.security.ContentSecurityPolicyMiddleware
CSP Settings SECURE_CSP, SECURE_CSP_REPORT_ONLY
Async Paginator from django.core.paginator import AsyncPaginator

Conclusion

Django 6.0 is a landmark release celebrating 20 years of Django. The built-in background tasks framework eliminates the need for Celery in many use cases. Template partials make templates more maintainable. CSP support improves security out of the box. And the modernized email API brings Django up to date with Python’s latest standards.

The transition from Django 5.x should be smooth for most projects, with the main consideration being the Python 3.12+ requirement.

Resources


   Reprint policy


《What's New in Django 6.0 - A Complete Guide》 by Isaac Zhou is licensed under a Creative Commons Attribution 4.0 International License
 Previous
Getting Started with uv - The Modern Python Package Manager Getting Started with uv - The Modern Python Package Manager
Learn how to use uv, the blazingly fast Python package and project manager written in Rust, to install Python, manage dependencies, and streamline your development workflow.
2026-01-01
Next 
Visualizing Data in Python: An In-Depth Comparison of Python's Top Libraries Visualizing Data in Python: An In-Depth Comparison of Python's Top Libraries
Check out my article on medium Data visualization is the process of converting data into visual formats such as charts
2023-02-06
  TOC