Django: what’s new in 6.0
Django 6.0 was released today, starting another release cycle for the loved and long-lived Python web framework (now 20 years old!). It comes with a mosaic of new features, contributed to by many, some of which I am happy to have helped with. Below is my pick of highlights from the release notes.
Upgrade with help from django-upgrade If you’re upgrading a project from Django 5.2 or earlier, please try my tool django-upgrade. It will automatically update old Django code to use new features, fixing some deprecation warnings for you, including five fixers for Django 6.0. (One day, I’ll propose django-upgrade to become an official Django project, when energy and time permit…)
Template partials There are four headline features in Django 6.0, which we’ll cover before other notable changes, starting with this one: The Django Template Language now supports template partials, making it easier to encapsulate and reuse small named fragments within a template file. Partials are sections of a template marked by the new {% partialdef %} and {% endpartialdef %} tags. They can be reused within the same template or rendered in isolation. Let’s look at examples for each use case in turn. Reuse partials within the same template The below template reuses a partial called filter_controls within the same template. It’s defined once at the top of the template, then used twice later on. Using a partial allows the template avoid repetition without pushing the content into a separate include file. < section id = videos > {% partialdef filter_controls %} < form > {{ filter_form }} form > {% endpartialdef %} {% partial filter_controls %} < ul > {% for video in videos %} < li > < h2 > {{ video.title }} h2 > ... li > {% endfor %} ul > {% partial filter_controls %} section > Actually, we can simplify this pattern further, by using the inline option on the partialdef tag, which causes the definition to also render in place: < section id = videos > {% partialdef filter_controls inline %} < form > {{ filter_form }} form > {% endpartialdef %} < ul > {% for video in videos %} < li > < h2 > {{ video.title }} h2 > ... li > {% endfor %} ul > {% partial filter_controls %} section > Reach for this pattern any time you find yourself repeating template code within the same template. Because partials can use variables, you can also use them to de-duplicate when rendering similar controls with different data. Render partials in isolation The below template defines a view_count partial that’s intended to be re-rendered in isolation. It uses the inline option, so when the whole template is rendered, the partial is included. The page uses htmx, via my django-htmx package, to periodically refresh the view count, through the hx-* attributes. The request from htmx goes to a dedicated view that re-renders the view_count partial. {% load django_htmx %} < html > < body > < h1 > {{ video.title }} h1 > < video width = 1280 height = 720 controls > < source src = " {{ video.file.url }} " type = "video/mp4" > Your browser does not support the video tag. video > {% partialdef view_count inline %} < section class = view-count hx-trigger = "every 1s" hx-swap = outerHTML hx-get = " {% url 'video-view-count' video.id %} " > {{ video.view_count }} views section > {% endpartialdef %} {% htmx_script %} body > html > The relevant code for the two views could look like this: from django.shortcuts import render def video ( request , video_id ): ... return render ( request , "video.html" , { "video" : video }) def video_view_count ( request , video_id ): ... return render ( request , "video.html#view_count" , { "video" : video }) The initial video view renders the full template video.html . The video_view_count view renders just the view_count partial, by appending #view_count to the template name. This syntax is similar to how you’d reference an HTML fragment by its ID in a URL. History htmx was the main motivation for this feature, as promoted by htmx creator Carson Gross in a cross-framework review post. Using partials definitely helps maintain “Locality of behaviour” within your templates, easing authoring, debugging, and maintenance by avoiding template file sprawl. Django’s support for template partials was initially developed by Carlton Gibson in the django-template-partials package, which remains available for older Django versions. The integration into Django itself was done in a Google Summer of Code project this year, worked on by student Farhan Ali and mentored by Carlton, in Ticket #36410. You can read more about the development process in Farhan’s retrospective blog post. Many thanks to Farhan for authoring, Carlton for mentoring, and Natalia Bidart, Nick Pope, and Sarah Boyce for reviewing!
Tasks framework The next headline feature we’re covering: Django now includes a built-in Tasks framework for running code outside the HTTP request–response cycle. This enables offloading work, such as sending emails or processing data, to background workers. Basically, there’s a new API for defining and enqueuing background tasks—very cool! Background tasks are a way of running code outside of the request-response cycle. They’re a common requirement in web applications, used for sending emails, processing images, generating reports, and more. Historically, Django has not provided any system for background tasks, and kind of ignored the problem space altogether. Developers have instead relied on third-party packages like Celery or Django Q2. While these systems are fine, they can be complex to set up and maintain, and often don’t “go with the grain” of Django. The new Tasks framework fills this gap by providing an interface to define background tasks, which task runner packages can then integrate with. This common ground allows third-party Django packages to define tasks in a standard way, assuming you’ll be using a compatible task runner to execute them. Define tasks with the new @task decorator: from django.tasks import task @task def resize_video ( video_id ): ... …and enqueue them for background execution with the Task.enqueue() method: from example.tasks import resize_video def upload_video ( request ): ... resize_video . enqueue ( video . id ) ... Execute tasks At this time, Django does not include a production-ready task backend, only two that are suitable for development and testing: ImmediateBackend - runs tasks synchronously, blocking until they complete.
- runs tasks synchronously, blocking until they complete. DummyBackend - does nothing when tasks are enqueued, but allows them to be inspected later. Useful for tests, where you can assert that tasks were enqueued without actually running them. For production use, you’ll need to use a third-party package that implements one, for which django-tasks, the reference implementation, is the primary option. It provides DatabaseBackend for storing tasks in your SQL database, a fine solution for many projects, avoiding extra infrastructure and allowing atomic task enqueuing within database transactions. We may see this backend merged into Django in due course, or at least become an official package, to help make Django “batteries included” for background tasks. To use django-tasks’ DatabaseBackend today, first install the package: uv add django-tasks Second, add these two apps to your INSTALLED_APPS setting: INSTALLED_APPS = [ # ... "django_tasks" , "django_tasks.backends.database" , # ... ] Third, configure DatabaseBackend as your tasks backend in the new TASKS setting: TASKS = { "default" : { "BACKEND" : "django_tasks.backends.database.DatabaseBackend" , }, } Fourth, run migrations to create the necessary database tables: $ ./manage.py migrate Finally, to run the task worker process, use the package’s db_worker management command: $ ./manage.py db_worker Starting worker worker_id = jWLMLrms3C2NcUODYeatsqCFvd5rK6DM queues = default This process runs indefinitely, polling for tasks and executing them, logging events as it goes: Task id=10b794ed-9b64-4eed-950c-fcc92cd6784b path=example.tasks.echo state=RUNNING Hello from test task! Task id=10b794ed-9b64-4eed-950c-fcc92cd6784b path=example.tasks.echo state=SUCCEEDED You’ll want to run db_worker in production, and also in development if you want to test background task execution. History It’s been a long path to get the Tasks framework into Django, and I’m super excited to see it finally available in Django 6.0. Jake Howard started on the idea for Wagtail, a Django-powered CMS, back in 2021, as they have a need for common task definitions across their package ecosystem. He upgraded the idea to target Django itself in 2024, when he proposed DEP 0014. As a member of the Steering Council at the time, I had the pleasure of helping review and accept the DEP. Since then, Jake has been leading the implementation effort, building pieces first in the separate django-tasks package before preparing them for inclusion in Django itself. This step was done under Ticket #35859, with a pull request that took nearly a year to review and land. Thanks to Jake for his perseverance here, and to all reviewers: Andreas Nüßlein, Dave Gaeddert, Eric Holscher, Jacob Walls, Jake Howard, Kamal Mustafa, @rtr1, @tcely, Oliver Haas, Ran Benita, Raphael Gaschignard, and Sarah Boyce. Read more about this feature and story in Jake’s post celebrating when it was merged.
Content Security Policy support Our third headline feature: Built-in support for the Content Security Policy (CSP) standard is now available, making it easier to protect web applications against content injection attacks such as cross-site scripting (XSS). CSP allows declaring trusted sources of content by giving browsers strict rules about which scripts, styles, images, or other resources can be loaded. I’m really excited about this, because I’m a bit of a security nerd who’s been deploying CSP for client projects for years. CSP is a security standard that can protect your site from cross-site scripting (XSS) and other code injection attacks. You set a content-security-policy header to declare which content sources are trusted for your site, and then browsers will block content from other sources. For example, you might declare that only scripts your domain are allowed, so an attacker who manages to inject a + This can be tedious and error-prone, hence using the report-only mode first to monitor violations might be useful, especially on larger projects. Anyway, deploying CSP right would be another post in itself, or even a book chapter, so we’ll stop here for now. For more info, check out that web.dev article and the MDN CSP guide. History CSP itself was proposed for browsers way back in 2004, and was first implemented in Mozilla Firefox version 4, released 2011. That same year, Django Ticket #15727 was opened, proposing adding CSP support to Django. Mozilla created django-csp from 2010, before the first public availability of CSP, using it on their own Django-powered sites. The first comment on Ticket #15727 pointed to django-csp, and the community basically rolled with it as the de facto solution. Over the years, CSP itself evolved, as did django-csp, with Rob Hudson ending up as its maintainer. Focusing on the package motivated to finally get CSP into Django itself. He made a draft PR and posted on Ticket #15727 in 2024, which I enjoyed helping review. He iterated on the PR over the next 13 months until it was finally merged for Django 6.0. Thanks to Rob for his heroic dedication here, and to all reviewers: Benjamin Balder Bach, Carlton Gibson, Collin Anderson, David Sanders, David Smith, Florian Apolloner, Harro van der Klauw, Jake Howard, Natalia Bidart, Paolo Melchiorre, Sarah Boyce, and Sébastien Corbin.
Positional arguments in django.core.mail APIs We’re now out of the headline features and onto the “minor” changes, starting with this deprecation related to the above email changes: django.core.mail APIs now require keyword arguments for less commonly used parameters. Using positional arguments for these now emits a deprecation warning and will raise a TypeError when the deprecation period ends: All optional parameters ( fail_silently and later) must be passed as keyword arguments to get_connection() , mail_admins() , mail_managers() , send_mail() , and send_mass_mail() .
and later) must be passed as keyword arguments to , , , , and . All parameters must be passed as keyword arguments when creating an EmailMessage or EmailMultiAlternatives instance, except for the first four ( subject , body , from_email , and to ), which may still be passed either as positional or keyword arguments. Previously, Django would let you pass all parameters positionally, which gets a bit silly and hard to read with long parameter lists, like: from django.core.mail import send_mail send_mail ( "🐼 Panda of the week" , "This week’s panda is Po Ping, sha-sha booey!" , "[email protected]" , [ "[email protected]" ], True , ) The final True doesn’t provide any clue what it means without looking up the function signature. Now, using positional arguments for those less-commonly-used parameters raises a deprecation warning, nudging you to write: from django.core.mail import send_mail send_mail ( subject = "🐼 Panda of the week" , body = "This week’s panda is Po Ping, sha-sha booey!" , from_email = "[email protected]" , [ "[email protected]" ], fail_silently = True , ) This change is appreciated for API clarity, and Django is generally moving towards using keyword-only arguments more often. django-upgrade can automatically fix this one for you, via its mail_api_kwargs fixer. Thanks to Mike Edmunds, again, for making this improvement in Ticket #36163.
... continue reading