If you’ve been sticking to the LTS version (Django 4.2 or 5.2) like a responsible adult, looking at the changelogs can feel like staring at a different framework. The days of installing five third-party packages just to send an async email or render a partial template are officially numbered down. Let's cut through the noise. Here are the actual game-changing updates in the "New Django", why they matter, and how to start using them. 1. Native Background Tasks (The "Finally!" Feature) The Problem: The Problem: For a decade, "How do I send an email without hanging the request?" had one answer: Install Redis. Install Celery. Configure a worker. Cry a little. It was overkill for 90% of use cases. Install Redis. Install Celery. Configure a worker. Cry a little. The Solution: The Solution: Django 6.0 introduces a native, lightweight Background Tasks framework. It’s not trying to kill Celery for complex enterprise pipelines, but for sending emails, resizing images, or data cleanup? It’s perfect. Background Tasks # settings.py # You can now swap backends easily. # For dev, it runs synchronously. For prod, plug in a database or Redis backend. TASKS_BACKEND = 'django.tasks.backends.database.DatabaseBackend' # tasks.py from django.tasks import task from django.core.mail import send_mail @task(priority='high', queue='urgent') def send_welcome_email(user_id): user = User.objects.get(pk=user_id) send_mail( 'Welcome!', 'Thanks for joining.', 'noreply@hackernoon.com', [user.email], ) # views.py def signup(request): # ... user creation logic ... # Fire and forget. No separate worker process needed in dev! send_welcome_email.enqueue(user.id) return HttpResponse("Check your inbox!") # settings.py # You can now swap backends easily. # For dev, it runs synchronously. For prod, plug in a database or Redis backend. TASKS_BACKEND = 'django.tasks.backends.database.DatabaseBackend' # tasks.py from django.tasks import task from django.core.mail import send_mail @task(priority='high', queue='urgent') def send_welcome_email(user_id): user = User.objects.get(pk=user_id) send_mail( 'Welcome!', 'Thanks for joining.', 'noreply@hackernoon.com', [user.email], ) # views.py def signup(request): # ... user creation logic ... # Fire and forget. No separate worker process needed in dev! send_welcome_email.enqueue(user.id) return HttpResponse("Check your inbox!") Why this is huge Why this is huge It lowers the barrier to entry. You don't need to be a DevOps expert to move a function off the main thread anymore. 2. Template Partials (HTMX Users Rejoice) The Problem: The Problem: With the rise of HTMX and Unpoly, we started slicing our templates/ folder into tiny, unmanageable files like _card.html, _button.html, and _modal.html just to render small snippets. It was messy. templates/ _card.html _button.html _modal.html The Solution: The Solution: Django 6.0 (building on concepts solidified in 5.x) now fully embraces Template Partials. You can define reusable blocks inside a template and render just that block. Template Partials inside just {% extends "base.html" %} {% block content %} <h1>Our Products</h1> <div id="product-list"> {% partialdef "card" %} <div class="product-card"> <h2>{{ product.name }}</h2> <button hx-post="{% url 'add_to_cart' product.id %}"> Add to Cart </button> </div> {% endpartialdef %} {% for product in products %} {% partial "card" %} {% endfor %} </div> {% endblock %} {% extends "base.html" %} {% block content %} <h1>Our Products</h1> <div id="product-list"> {% partialdef "card" %} <div class="product-card"> <h2>{{ product.name }}</h2> <button hx-post="{% url 'add_to_cart' product.id %}"> Add to Cart </button> </div> {% endpartialdef %} {% for product in products %} {% partial "card" %} {% endfor %} </div> {% endblock %} The Render Logic: In your view, you can now request just the "card" partial: The Render Logic: just # views.py def product_search(request): # If it's an HTMX request, render only the partial! if request.headers.get('HX-Request'): return render(request, "products.html#card", context) return render(request, "products.html", context) # views.py def product_search(request): # If it's an HTMX request, render only the partial! if request.headers.get('HX-Request'): return render(request, "products.html#card", context) return render(request, "products.html", context) Why this is huge Why this is huge It keeps related code together (Locality of Behaviour) and eliminates the file-sprawl nightmare of modern frontend stacks. 3. Native Content Security Policy (CSP) The Problem: The Problem: XSS (Cross-Site Scripting) is still the boogeyman. Configuring CSP headers usually meant installing django-csp and wrestling with middleware ordering. django-csp The Solution: The Solution: Security is now first-class. Django 6.0 includes a built-in CSP Middleware with nonce support. CSP Middleware # settings.py MIDDLEWARE = [ # ... "django.middleware.security.ContentSecurityPolicyMiddleware", ] # It handles nonces automatically! CSP_SCRIPT_SRC = ["'self'", "'nonce-{nonce}'"] # settings.py MIDDLEWARE = [ # ... "django.middleware.security.ContentSecurityPolicyMiddleware", ] # It handles nonces automatically! CSP_SCRIPT_SRC = ["'self'", "'nonce-{nonce}'"] HTML HTML <script nonce="{{ request.csp_nonce }}"> console.log("I am a safe, approved script."); </script> <script nonce="{{ request.csp_nonce }}"> console.log("I am a safe, approved script."); </script> ICYMI: The Best of Django 5.2 Since many of you might be jumping straight from 4.2 LTS to 6.0, you might have missed Composite Primary Keys, which finally landed in Django 5.2 earlier this year. Composite Primary Keys If you deal with legacy databases or time-series data, this is the one you've been waiting for. from django.db import models class Membership(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE) course = models.ForeignKey(Course, on_delete=models.CASCADE) date_joined = models.DateField() class Meta: # No more unnecessary 'id' field! # The PK is now the combination of student and course. constraints = [ models.CompositePrimaryKey('student', 'course'), ] from django.db import models class Membership(models.Model): student = models.ForeignKey(Student, on_delete=models.CASCADE) course = models.ForeignKey(Course, on_delete=models.CASCADE) date_joined = models.DateField() class Meta: # No more unnecessary 'id' field! # The PK is now the combination of student and course. constraints = [ models.CompositePrimaryKey('student', 'course'), ] The Verdict Django 6.0 isn't just "more features." It’s a shift in philosophy. For years, Django was the "batteries included" framework, but you still had to buy the batteries for Async Tasks and Frontend interactivity separately. With 6.0, Django is reclaiming the full stack. Future Gazing (Django 6.x and 7.0): Future Gazing (Django 6.x and 7.0): What's next? The steering council has hinted at: Deeper Async Integration: The ORM is mostly async now, but look for async admin views in the near future.Typed Django: Stricter type hints in the core, making mypy checks strictly standard. Deeper Async Integration: The ORM is mostly async now, but look for async admin views in the near future. Deeper Async Integration: Typed Django: Stricter type hints in the core, making mypy checks strictly standard. Typed Django: mypy Next Step for You: Next Step for You: Don't just read this. Go to your requirements.txt, bump that version number, and try running the new startproject to see the new default skeleton. requirements.txt startproject