Django 6.0: What I Actually Think After Using It in Production

Django turned 20 this year. That's a long time for a web framework to stay relevant, and honestly? The way it has stayed relevant tells you something important about its philosophy: it absorbs the ecosystem rather than fighting it. Everything that gets popular as a third-party package — eventually, if the community agrees it belongs, it gets pulled in. Django 6.0 is a textbook example of that.

Four headline features. All four were previously available as external packages. All four arguably belong in core. Let's go through them honestly.


The four things that actually matter

# Feature What it replaces
01 Background Tasks Celery / RQ for simple cases
02 Template Partials django-template-partials
03 Native CSP django-csp middleware package
04 Modern Email API The old, encoding-broken EmailMessage

Background tasks: the one everyone's excited about

This is the headline feature, and it deserves the attention. Django now has a native way to define and enqueue background tasks — no Celery, no RQ, no Redis dependency for simple use cases.

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

@task
def notify_user(user_id, subject, message):
    user = User.objects.get(pk=user_id)
    send_mail(subject, message, None, [user.email])

# In your view:
notify_user.enqueue(
    user_id=request.user.pk,
    subject="Your export is ready",
    message="Download it here: ...",
)

This is genuinely nice. The API is clean, the decorator is obvious, and the intent reads clearly. For projects that were previously spawning Celery just to send a post-registration email, this is a real improvement in setup complexity.

But here's the honest part: Django handles the definition and queuing. It does not ship a worker. You still need external infrastructure to actually execute tasks — either a custom management command calling consume_tasks, or a third-party backend. There's no built-in DB backend either. Community reaction has been split — some love the clean API, others wanted a batteries-included worker they could actually run with manage.py. That criticism is fair.

My take: if you're running anything production-grade, you probably want a real task queue anyway. The value of this feature is the standardized API — once backends proliferate, you'll be able to swap between them without changing your task code. That's the win. Think of it less as "replace Celery" and more as "WSGI for task queues."


Template partials: obvious in retrospect

I've been using django-template-partials by Carlton Gibson for the past year. It's one of those packages you install immediately on every new project and wonder why it isn't in core. Now it is.

{# Define a partial inline #}
{% partialdef user-avatar %}
  <div class="avatar">
    <img src="{{ user.avatar_url }}" alt="{{ user.name }}">
    <span>{{ user.name }}</span>
  </div>
{% endpartialdef %}

{# Reuse it elsewhere in the same template #}
{% partial user-avatar %}

Combined with HTMX, this becomes genuinely powerful — you can render just the partial for a targeted DOM swap without a separate view. This is the kind of thing that makes Django feel modern without needing to reach for a JS framework. If you're building HTMX-heavy UIs, this is probably your most-used feature from this release.


Native CSP: long overdue

Content Security Policy is one of those security hygiene things that most Django projects either skip entirely or implement with django-csp. The middleware works, but it's another dependency and another config surface to keep in sync. Having it in core means it's in the Django security checklist, it gets documented alongside other security settings, and it gets tested by the Django team.

# settings.py
SECURE_CSP = {
    "default-src": ["'self'"],
    "script-src": ["'self'", "'nonce'"],
    "img-src": ["'self'", "data:", "https://cdn.example.com"],
}

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

Nonces are supported through the csp() context processor. If you're migrating from django-csp, there's a migration guide in the release notes — not a direct drop-in replacement, but the config shape is familiar enough.


The things you'll hit during upgrade

Python 3.12 is the new minimum. This is the change that will actually block most teams from upgrading immediately. It's not that 3.12 is bad — it's fast, the error messages are dramatically better, and the deprecation warnings that were optional in earlier versions are now unavoidable. But if you're on Oracle Linux or an OS with a pinned Python, this requires coordination. Plan for it.

MariaDB users: 10.5 is dropped. You need 10.6 or higher. Check your versions before planning the upgrade window. This catches people off guard more than the Python version bump.

The email API changes have some sharp edges if you've subclassed EmailMessage or EmailMultiAlternatives. Internal implementation changed significantly — any custom subclasses that override private underscore methods need to be audited. Run your test suite with python -Wd to catch deprecation warnings before they become errors.


The smaller things worth knowing

The ORM got a quiet but useful improvement: GeneratedField values and expression-assigned fields are now refreshed from the database after save() on backends that support RETURNING (PostgreSQL, SQLite, Oracle). If you've been doing manual refresh_from_db() calls after saves with generated fields, you can drop those.

The forloop.length variable is now available in template for loops. I know. I know. You've been computing it yourself for fifteen years. It's here now.

If you use manage.py shell, Django 6.0 adds more automatic imports — timezone, additional utilities alongside models. Small thing, but if you spend any time in the shell it's a quality-of-life improvement.

Async pagination is now real: AsyncPaginator and AsyncPage exist. If you've been wrapping Paginator in sync_to_async inside async views, that workaround is no longer needed.


Should you upgrade now?

On Django 5.2 LTS: It's supported until April 2028. There's no urgency. The Python 3.12 requirement is the right time to upgrade — do both at once rather than two separate migration windows.

On Django 4.2 LTS: Support ends April 2026. You're out of runway. Upgrade to 5.2 if you can't do the Python 3.12 jump yet, then to 6.0 when you can.

Starting a new project today: Use 6.0. The background tasks API and template partials are worth it alone, and you won't carry forward any upgrade debt.


Bottom line

Django 6.0 is not a revolution. It's Django doing what it does best — absorbing proven patterns from the ecosystem and making them first-class. The background tasks framework will matter more once the backend ecosystem matures. Template partials matter right now. CSP in core is overdue. None of this breaks your existing code in surprising ways. That's a Django release.


If you're planning an upgrade, run python -Wd manage.py test first to surface deprecation warnings from 5.x. The official docs have a solid upgrade guide, and django-upgrade by Adam Johnson can automate many of the mechanical changes.

Share This Post

Comments (0)

Leave a Comment

No comments yet. Be the first to comment!