Команда Python for Devs подготовила перевод обзора обновлений Django 6.0. В свежем релизе фреймворк усиливает совместимость между СУБД, упрощает работу с email, улучшает ORM, добавляет удобства в шаблонах и снижает риск «выгорания» первичных ключей.

Сегодня вышел Django 6.0, открыв новый цикл развития этого любимого и живущего уже 20 лет Python-фреймворка для веба. В релиз вошло много новых возможностей, к которым приложили руку многие участники сообщества, и некоторым из них я тоже рад был помочь. Ниже — мой выбор самых интересных пунктов из релиз-ноутсов.
Обновление с помощью django-upgrade
Если вы обновляете проект с Django 5.2 или более ранней версии, советую воспользоваться моим инструментом django-upgrade. Он автоматически обновит старый код Django под новые возможности, убрав часть deprecated предупреждений, включая пять фиксеров специально для Django 6.0. (Однажды я предложу включить django-upgrade в официальные проекты Django, когда появятся силы и время…)
Частичные шаблоны
В Django 6.0 есть четыре ключевые возможности. Прежде чем перейти к другим изменениям, начнем с первой:
Django Template Language теперь поддерживает частичные шаблоны (template partials), что упрощает выделение и повторное использование небольших именованных фрагментов внутри одного файла шаблона.
Частичные шаблоны — это блоки в шаблоне, отмеченные новыми тегами {% partialdef %} и {% endpartialdef %}. Их можно повторно использовать в том же шаблоне или рендерить отдельно. Рассмотрим примеры для каждого варианта.
Повторное использование частичных шаблонов в одном файле
Ниже показан шаблон, который дважды использует частичный шаблон с именем filter_controls. Он определяется один раз в начале, а затем вызывается дважды дальше по тексту. Частичный шаблон позволяет не дублировать код и при этом не выносить этот фрагмент в отдельный include-файл.
<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>
На самом деле этот шаблон можно упростить ещё больше, используя параметр inline у тега partialdef. В этом режиме определение частичного шаблона сразу же рендерится на месте:
<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>
К этому приёму стоит обращаться всякий раз, когда вы повторяете один и тот же фрагмент кода внутри одного шаблона. Поскольку частичные шаблоны могут использовать переменные, их удобно применять и для устранения дублирования при рендеринге похожих элементов управления, которые отличаются только данными.
Рендеринг частичных шаблонов отдельно
В примере ниже определяется частичный шаблон view_count, предназначенный для отдельного повторного рендеринга. Он использует параметр inline, поэтому при первоначальном рендеринге всего шаблона этот фрагмент сразу включается в страницу.
Страница использует htmx через мой пакет django-htmx, чтобы периодически обновлять счётчик просмотров с помощью атрибутов hx-*. Запрос htmx уходит в отдельное представление, которое рендерит только частичный шаблон view_count.
{% load django_htmx %}
<!doctype html>
<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>
Код двух представлений выглядит так:
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})
Первое представление video рендерит весь шаблон video.html. Представление video_view_count рендерит только частичный шаблон view_count, добавляя #view_count к имени шаблона. Такая запись напоминает обращение к HTML-фрагменту по его ID в URL.
История
Главным мотиватором для появления этой возможности был htmx. Создатель htmx Карсон Гросс продвигал идею частичных шаблонов в обзоре разных фреймворков. Partials действительно помогают сохранять «локальность поведения» в шаблонах, упрощая создание, отладку и поддержку кода и не допуская разрастания числа шаблонов.
Поддержка частичных шаблонов в Django изначально появилась в пакете django-template-partials, который разработал Карлтон Гибсон. Этот пакет по-прежнему доступен для более старых версий Django. Включение функциональности в сам Django было выполнено в рамках проекта Google Summer of Code в этом году. Над задачей работал студент Фархан Али под менторством Карлтона по тикету №36410. Подробнее о процессе разработки можно прочитать в ретроспективном посте Фархана. Большая благодарность Фархану за реализацию, Карлтону за наставничество и Наталье Бидарт, Нику Попу и Саре Бойс за ревью!
Фреймворк задач
Следующая ключевая возможность:
В Django теперь есть встроенный фреймворк Tasks для выполнения кода вне цикла запрос–ответ. Это позволяет переносить работу вроде отправки писем или обработки данных на фоновые воркеры. По сути, появился новый API для определения и постановки фоновых задач в очередь — отличное обновление.
Фоновые задачи дают возможность выполнять код вне контекста обработки HTTP-запроса. Это частая потребность веб-приложений: отправка писем, обработка изображений, генерация отчетов и многое другое.
Исторически Django не предоставлял собственной системы для фоновых задач и фактически обходил эту область стороной. Разработчики использовали сторонние решения вроде Celery или Django Q2. Эти инструменты рабочие, но могут быть сложными в настройке и сопровождении, а зачастую и не слишком «вписываться» в стиль Django.
Новый фреймворк Tasks закрывает этот пробел, предлагая единый интерфейс для определения фоновых задач, с которым уже могут интегрироваться пакеты, реализующие запуск задач. Эта единая точка интеграции позволяет сторонним пакетам Django определять задачи стандартизированным способом, предполагая, что вы будете использовать совместимый раннер задач для их выполнения.
Определить задачу можно с помощью нового декоратора @task:
from django.tasks import task
@task
def resize_video(video_id): ...
…а поставить задачу в очередь для фонового выполнения можно методом Task.enqueue():
from example.tasks import resize_video
def upload_video(request):
...
resize_video.enqueue(video.id)
...
Выполнение задач
Сейчас Django не включает продакшн-готовый backend для задач, а лишь два варианта, подходящих для разработки и тестирования:
ImmediateBackend — выполняет задачи синхронно, блокируя поток до завершения.
DummyBackend — ничего не делает при постановке задачи в очередь, но позволяет позже проверить, какие задачи были поставлены. Полезно в тестах, когда нужно убедиться, что задача была поставлена в очередь, но при этом не запускать её.
Для продакшна вам понадобится сторонний пакет, реализующий backend. Основной вариант — django-tasks, эталонная реализация. Он предоставляет DatabaseBackend, который хранит задачи в вашей SQL-базе. Для многих проектов это удобное решение: не нужно дополнительной инфраструктуры, а постановка задач может быть атомарной в рамках транзакций базы данных. В перспективе этот backend могут включить в сам Django или, как минимум, сделать официальным пакетом, чтобы Django действительно был «с батарейками» в части фоновых задач.
Чтобы использовать DatabaseBackend из django-tasks уже сейчас, сначала установите пакет:
uv add django-tasks
Во-вторых, добавьте эти два приложения в INSTALLED_APPS:
INSTALLED_APPS = [
# ...
"django_tasks",
"django_tasks.backends.database",
# ...
]
В-третьих, укажите DatabaseBackend как backend задач в новом параметре TASKS:
TASKS = {
"default": {
"BACKEND": "django_tasks.backends.database.DatabaseBackend",
},
}
В-четвертых, выполните миграции, чтобы создать необходимые таблицы в базе данных:
$ ./manage.py migrate
Наконец, чтобы запустить воркер, используйте management-команду db_worker из пакета:
$ ./manage.py db_worker
Starting worker worker_id=jWLMLrms3C2NcUODYeatsqCFvd5rK6DM queues=default
Этот процесс работает непрерывно: он опрашивает базу на наличие задач, выполняет их и логирует происходящее:
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
Команду db_worker стоит запускать в продакшне, а также в локальной разработке, если вы хотите тестировать выполнение фоновых задач.
История
Путь к появлению фреймворка Tasks в Django был долгим, и очень приятно видеть его наконец в составе Django 6.0. Джейк Ховард начал работать над этой идеей в 2021 году для Wagtail, CMS на базе Django, где возникла потребность в единых определениях задач для экосистемы пакетов. В 2024 году он расширил идею до уровня самого Django и предложил DEP 0014. Я тогда входил в Steering Council и имел удовольствие участвовать в ревью и принятии этого DEP.
С тех пор Джейк вел основную работу по реализации: сначала собирал отдельные части в отдельном пакете django-tasks, а затем готовил их для включения в Django. Этот этап прошел в рамках тикета №35859, а pull request к нему занял почти год обсуждений, ревью и доработок. Огромное спасибо Джейку за выдержку и настойчивость, а также всем, кто участвовал в ревью: Andreas Nüßlein, Dave Gaeddert, Eric Holscher, Jacob Walls, Jake Howard, Kamal Mustafa, @rtr1, @tcely, Oliver Haas, Ran Benita, Raphael Gaschignard и Sarah Boyce.
Подробнее об этой возможности и её истории можно прочитать в посте Джейка, опубликованном после того, как фича была смержена.
Поддержка Content Security Policy
Третья ключевая возможность:
В Django появилась встроенная поддержка стандарта Content Security Policy (CSP), что упрощает защиту веб-приложений от атак, связанных с внедрением контента, включая межсайтовый скриптинг (XSS). CSP позволяет объявлять доверенные источники контента, задавая браузеру строгие правила о том, какие скрипты, стили, изображения и другие ресурсы могут быть загружены.
Для меня это особенно приятно, потому что я люблю темы безопасности и много лет внедряю CSP в проектах клиентов.
CSP — это стандарт безопасности, который помогает защищать сайт от XSS и других атак, основанных на вставке кода. В заголовке content-security-policy вы объявляете, каким источникам контента доверяет сайт, и браузер блокирует ресурсы из других источников. Например, можно разрешить загружать скрипты только с вашего домена. Тогда злоумышленник, сумевший внедрить тег <script> с ссылкой на evil.com, ничего бы не добился: браузер просто не загрузил бы его.
Раньше Django не имел встроенной поддержки CSP, и разработчикам приходилось писать свою реализацию или использовать сторонние пакеты вроде популярного django-csp. Но это создавало неудобства: сторонние пакеты не имели общего API, на который можно было бы опираться для интеграции.
Новая реализация CSP в Django предоставляет все ключевые возможности django-csp, но через более аккуратный и «джанговый» API. Начинаем с добавления ContentSecurityPolicyMiddleware в MIDDLEWARE:
MIDDLEWARE = [
# ...
"django.middleware.csp.ContentSecurityPolicyMiddleware",
# ...
]
Размещайте его рядом с SecurityMiddleware, потому что оба добавляют к ответам заголовки, связанные с безопасностью. (SecurityMiddleware у вас точно включен, верно?)
Затем настройте свою CSP-политику с помощью новых параметров:
SECURE_CSP для заголовка content-security-policy — он включает активное, применяемое браузером правило.
SECURE_CSP_REPORT_ONLY для заголовка content-security-policy-report-only — браузер не блокирует нарушения, а только отправляет отчеты на указанный endpoint. Это полезно для тестирования политики перед тем, как начать ее применять.
Например, чтобы использовать основанную на nonce строгую политику, рекомендованную web.dev, можно начать с такой настройки:
from django.utils.csp import CSP
SECURE_CSP_REPORT_ONLY = {
"script-src": [CSP.NONCE, CSP.STRICT_DYNAMIC],
"object-src": [CSP.NONE],
"base-uri": [CSP.NONE],
}
Перечисление CSP содержит константы для директив CSP, что помогает избегать опечаток.
Такая политика очень строгая и «сломает» большинство существующих сайтов, если применить её сразу, потому что она требует nonce — об этом ниже. Поэтому в примере используется режим report-only, чтобы сначала выявить все места, требующие исправлений. Позже можно будет перейти к SECURE_CSP и включить реальное применение политики.
На этом два базовых шага настройки CSP в Django завершены.
Генерация nonce
Ключевая часть новой возможности заключается в том, что генерация nonce теперь встроена в Django при использовании CSP middleware. Nonce в CSP позволяет помечать конкретные теги <script> и <style> как доверенные с помощью атрибута nonce:
<script src=/static/app.js type=module nonce=55vsH4w7ATHB85C3MbPr_g></script>
Значение nonce генерируется случайным образом для каждого запроса и включается в CSP-заголовок. Злоумышленник, пытающийся внедрить свой контент, не сможет угадать nonce, поэтому браузер будет доверять только тем тегам, где указан корректный nonce. Поскольку генерация nonce теперь является частью Django, сторонние пакеты могут опираться на неё для своих <script> и <style> тегов, и всё будет продолжать работать, если вы включите CSP с nonce.
Nonce сегодня считаются рекомендуемым способом использования CSP, так как они избегают проблем подходов на основе списков разрешённых источников. Поэтому в примере выше эта политика их и включает. Чтобы перейти на использование nonce, нужно добавить nonce к своим <script> и <style> тегам, выполнив следующие шаги.
Сначала подключите новый template context processor csp в настройке TEMPLATES:
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"OPTIONS": {
"context_processors": [
# ...
"django.template.context_processors.csp",
],
},
},
]
Затем добавьте nonce="{{ csp_nonce }}" к тегам <script> и <style>:
- <script src="{% static 'app.js' %}" type="module"></script>
+ <script src="{% static 'app.js' %}" type="module" nonce="{{ csp_nonce }}"></script>
Работа может оказаться утомительной и подверженной ошибкам, поэтому стоит сначала включить report-only режим и посмотреть, где возникают нарушения — особенно на крупных проектах.
Вообще, развёртывание корректной CSP заслуживает отдельного поста или даже главы книги, так что пока остановимся здесь. За дополнительной информацией можно обратиться к статье на web.dev и руководству CSP на MDN.
История
Идея CSP была предложена для браузеров ещё в 2004 году, а впервые реализована в Mozilla Firefox 4, вышедшем в 2011. В том же году был открыт тикет Django №15727 с предложением добавить поддержку CSP в сам Django. Mozilla начала разработку django-csp в 2010 году, ещё до появления CSP в браузерах, и использовала его на своих сайтах, построенных на Django. Первый комментарий в тикете №15727 ссылался на django-csp, и сообщество фактически признало его стандартным решением.
Со временем развивался сам стандарт CSP, а вместе с ним и django-csp. В итоге мейнтейнером django-csp стал Роб Хадсон. Работа над пакетом стала одним из драйверов, подтолкнувших к тому, чтобы наконец внедрить CSP прямо в Django. В 2024 году он создал черновой PR и написал об этом в тикете №15727, и я с интересом участвовал в его ревью. Роб продолжал дорабатывать PR в течение следующих 13 месяцев, пока он, наконец, не был принят в Django 6.0. Огромная благодарность Робу за его колоссальную настойчивость, а также всем рецензентам: 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 и Sébastien Corbin.
Обновления Email API
Четвёртая и последняя крупная новинка:
Обработка email в Django теперь опирается на современный email API из Python, появившийся в Python 3.6. Этот API, построенный вокруг класса email.message.EmailMessage, предоставляет более чистый и корректно работающий с Unicode интерфейс для составления и отправки писем.
Это серьёзное изменение, но проекты, использующие базовые возможности отправки почты, скорее всего, ничего не почувствуют. Всё ещё можно пользоваться функцией Django send_mail() и классом EmailMessage так же, как и раньше, например:
from django.core.mail import EmailMessage
email = EmailMessage(
subject="? Need more bamboo",
body="We are desperately low, please restock before the pandas find out!",
from_email="zookeeper@example.com",
to=["supplies@example.com"],
)
email.attach_file("/media/bamboo_cupboard.jpg")
email.send()
Ключевое изменение в том, что внутри, при вызове send() у объекта Django EmailMessage, он теперь преобразуется в новый тип Python email.message.EmailMessage перед отправкой.
Обновление даёт такие преимущества:
Меньше багов: множество проблемных угловых кейсов в старом email API Python уже исправлены в новом.
Меньше костылей в Django: ряд обходных решений и исправлений безопасности в коде отправки почты стал не нужен.
Более удобный API: новый интерфейс поддерживает разные приятные мелочи, например встроенные вложения, как в примере ниже.
Более простые встроенные вложения с MIMEPart
Метод Django EmailMessage.attach() позволяет прикреплять файл как обычное вложение. Почтовые сообщения также поддерживают изображения как встроенные вложения, которые могут отображаться прямо в HTML-теле письма.
Раньше для добавления встроенных вложений тоже можно было использовать EmailMessage.attach(), но работать с этим было не очень удобно, поскольку использовался устаревший класс. Теперь можно передать в метод объект Python email.message.MIMEPart и добавить встроенное вложение всего в несколько шагов:
import email.utils
from email.message import MIMEPart
from django.core.mail import EmailMultiAlternatives
message = EmailMultiAlternatives(
subject="Cute Panda Alert",
body="Here's a cute panda picture for you!",
from_email="cute@example.com",
to=["fans@example.com"],
)
with open("panda.jpg", "rb") as f:
panda_jpeg = f.read()
cid = email.utils.make_msgid()
inline_image = MIMEPart()
inline_image.set_content(
panda_jpeg,
maintype="image",
subtype="jpeg",
disposition="inline",
cid=cid,
)
message.attach(inline_image)
message.attach_alternative(
f'<h1>Cute panda baby alert!</h1><img src="cid:{cid[1:-1]}">',
"text/html",
Это не самый простой интерфейс, но он раскрывает все возможности базовой почтовой системы, и выглядит куда лучше прежнего решения.
История
Новый email API появился в Python как предварительный в версии 3.4 (2014 год) и стал стабильным в версии 3.6 (2016 год). Устаревший API при этом никогда не планировали к удалению, поэтому у Django не было крайнего срока для перехода на новый механизм отправки писем.
В 2024 году Майк Эдмундс написал в (старую) рассылку django-developers и предложил переход на новый API, представив убедительное обоснование и план работ. Из этого обсуждения вырос тикет №35581, над которым он работал восемь месяцев, пока изменения не были слиты. Большое спасибо Майку за эту работу и Саре Бойс за ревью! Email не выглядит яркой или модной возможностью, но это критически важный канал связи почти для любого Django-проекта, так что это действительно важный вклад.
Позиционные аргументы в API django.core.mail
Мы закончили с ключевыми новинками и переходим к «минорным» изменениям, начиная с этой деприкации, связанной с описанными выше обновлениями email:
API django.core.mail теперь требуют использования именованных аргументов для реже применяемых параметров. Использование позиционных аргументов для них теперь вызывает предупреждение о деприкации и превратится в TypeError после окончания переходного периода:
Все необязательные параметры (начиная с fail_silently и далее) должны передаваться как именованные аргументы в get_connection(), mail_admins(), mail_managers(), send_mail() и send_mass_mail().
При создании экземпляров EmailMessage или EmailMultiAlternatives все параметры должны передаваться как именованные, кроме первых четырёх (subject, body, from_email и to). Эти четыре по-прежнему можно передавать как позиционно, так и по имени.
Раньше Django позволял передавать все параметры позиционно, что становилось довольно нелепо и трудно читается при длинных списках параметров, например:
from django.core.mail import send_mail
send_mail(
"? Panda of the week",
"This week’s panda is Po Ping, sha-sha booey!",
"updates@example.com",
["adam@example.com"],
True,
)
Последний аргумент True никак не намекает на свой смысл, пока не заглянешь в сигнатуру функции. Теперь использование позиционных аргументов для редко применяемых параметров вызывает предупреждение о деприкации и подталкивает писать так:
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="updates@example.com",
["adam@example.com"],
fail_silently=True,
)
Это изменение улучшает читаемость API, и Django в целом движется в сторону более частого использования аргументов-только-по-ключу. django-upgrade может автоматически исправить это за вас через фиксер mail_api_kwargs.
Спасибо Майку Эдмундсу ещё раз за улучшение в тикете №36163.
Расширенные автоматические импорты в shell
Далее:
Распространённые утилиты, такие как django.conf.settings, теперь автоматически импортируются в интерактивную оболочку по умолчанию.
Одной из ключевых возможностей Django 5.2 были автоматические импорты моделей в shell, благодаря которым ./manage.py shell автоматически подтягивал все модели проекта. Развивая это улучшение DX, Django 6.0 теперь импортирует и другие часто используемые утилиты. Полный список можно увидеть, выполнив ./manage.py shell с параметром -v 2:
$ ./manage.py shell -v 2
6 objects imported automatically:
from django.conf import settings
from django.db import connection, models, reset_queries
from django.db.models import functions
from django.utils import timezone
...
(Это проект без моделей, поэтому перечислены только утилиты.)
Итак, теперь автоматически доступны:
settings, полезный для проверки конфигурации во время исполнения:
In [1]: settings.DEBUG
Out[1]: False
connection и reset_queries(), удобные для проверки выполненных запросов:
In [1]: Book.objects.select_related('author')
Out[1]: <QuerySet []>
In [2]: connection.queries
Out[2]:
[{'sql': 'SELECT "example_book"."id", "example_book"."title", "example_book"."author_id", "example_author"."id", "example_author"."name" FROM "example_book" INNER JOIN "example_author" ON ("example_book"."author_id" = "example_author"."id") LIMIT 21',
'time': '0.000'}]
In [1]: Book.objects.annotate(
...: title_lower=functions.Lower("title")
...: ).filter(
...: title_lower__startswith="a"
...: ).count()
Out[1]: 71
timezone, полезный для использования утилит Django, работающих с часовыми поясами:
In [1]: timezone.now()
Out[1]: datetime.datetime(2025, 12, 1, 23, 42, 22, 558418, tzinfo=datetime.timezone.utc)
При этом по-прежнему можно расширять набор автоматических импортов по своему усмотрению, как описано на странице документации «How to customize the shell command».
Сальво Полицци добавил исходную возможность автоматического импорта в Django 5.2. Теперь он вернулся с расширением импорта для Django 6.0 в тикете №35680. Спасибо всем участникам обсуждения на форуме, помогшим определиться, какие импорты включить, а также Наталии Бидарт и Саре Бойс за ревью!
Динамическое обновление полей при save()
Переходим к серии улучшений ORM, начиная с крупного изменения:
GeneratedFields и поля, которым перед сохранением присваиваются выражения, теперь обновляются из базы данных после save() на тех СУБД, которые поддерживают конструкцию RETURNING (SQLite, PostgreSQL и Oracle). На СУБД без поддержки RETURNING (MySQL и MariaDB) такие поля помечаются как отложенные, чтобы обновиться при последующем обращении.
В моделях Django есть три случая, когда значение поля может формироваться базой данных:
1. Параметр db_default, который позволяет базе сгенерировать значение по умолчанию при создании экземпляра:
from django.db import models
from django.db.models.functions import Now
class Video(models.Model):
...
created = models.DateTimeField(db_default=Now())
2. Тип поля GeneratedField, которое всегда вычисляется базой данных на основе других полей экземпляра:
from django.db import models
from django.db.models.functions import Concat
class Video(models.Model):
...
full_title = models.GeneratedField(
models.TextField(),
expression=Concat(
"title",
models.Value(" - "),
"subtitle",
),
)
3. Присвоение полям значений-выражений перед сохранением:
from django.db import models
from django.db.models.functions import Now
class Video(models.Model):
...
last_updated = models.DateTimeField()
video = Video.objects.get(id=1)
...
video.last_updated = Now()
video.save()
Раньше только первый способ, через db_default, приводил к тому, что Django обновлял значение поля после сохранения. В двух других случаях после save() у вас оставалось либо старое значение, либо сам объект выражения, поэтому, чтобы получить обновлённое значение, приходилось вручную делать Model.refresh_from_db(). Это легко забыть, плюс требуется дополнительный запрос.
Теперь Django использует SQL-выражение RETURNING, чтобы сохранить объект и получить новые значения динамических полей одним запросом на тех СУБД, которые это поддерживают (SQLite, PostgreSQL, Oracle). Вызов save() может отправить запрос вида:
UPDATE "example_video"
SET "last_updated" = NOW()
WHERE "example_video"."id" = 1
RETURNING "example_video"."last_updated"
Django помещает возвращённое значение в поле модели, так что его можно прочесть сразу после сохранения:
video = Video.objects.get(id=1)
...
video.last_updated = Now()
video.save()
print(video.last_updated) # Updated value from the database
На СУБД без поддержки RETURNING (MySQL и MariaDB) Django теперь помечает динамические поля как отложенные после сохранения. Поэтому последующее обращение к таким полям, как в примере выше, автоматически вызовет Model.refresh_from_db(). Это гарантирует, что вы всегда получите обновлённое значение, даже если для этого потребуется дополнительный запрос.
История
Эта возможность была предложена в тикете №27222 ещё в 2016 году Ансси Кяэриайненом. Затем она почти девять лет лежала без движения, но в этом году Саймон Шарет из команды ORM поднял её, нашёл рабочее решение и довёл до завершения. Спасибо Саймону за постоянный прогресс в ORM и всем ревьюерам: David Sanders, Jacob Walls, Mariusz Felisiak, nessita, Paolo Melchiorre, Simon Charette и Tim Graham.
Универсальный агрегат StringAgg
Следующее изменение в ORM:
Новый агрегат StringAgg возвращает входные значения, объединённые в строку и разделённые указанным разделителем. Ранее он работал только в PostgreSQL.
Этот агрегат часто используют, например, чтобы получить список связанных элементов, объединённый запятыми. Прежде он был доступен лишь для PostgreSQL и находился в django.contrib.postgres:
from django.contrib.postgres.aggregates import StringAgg
from example.models import Video
videos = Video.objects.annotate(
chapter_ids=StringAgg("chapter", delimiter=","),
)
for video in videos:
print(f"Video {video.id} has chapters: {video.chapter_ids}")
…и мог выдавать результат вроде:
Video 104 has chapters: 71,72,74
Video 107 has chapters: 88,89,138,90,91,93
Теперь этот агрегат доступен на всех поддерживаемых Django СУБД и импортируется из django.db.models:
from django.db.models import StringAgg, Value
from example.models import Video
videos = Video.objects.annotate(
chapter_ids=StringAgg("chapter", delimiter=Value(",")),
)
for video in videos:
print(f"Video {video.id} has chapters: {video.chapter_ids}")
Обратите внимание, что аргумент delimiter теперь должен быть обёрнут в выражение Value() для литеральных строк. Такой подход позволяет использовать в качестве разделителя функции базы данных или поля, если это нужно.
Хотя большинство проектов Django остаются на PostgreSQL, появление этого агрегата на всех backend'ах улучшает совместимость между СУБД и позволяет сторонним библиотекам использовать его, не ограничивая поддержку баз данных.
История
PostgreSQL-специфичный StringAgg появился ещё в Django 1.9 (2015) благодаря Андрию Соколовскому в тикете №24301. В тикете №35444 Крис Муттиг предложил добавить параметр Aggregate.order_by, который используется StringAgg для указания порядка соединяемых элементов. Побочным эффектом это позволило обобщить StringAgg для всех backend'ов.
Спасибо Крису за предложение и реализацию, а также ревьюерам: Paolo Melchiorre, Sarah Boyce и Simon Charette.
BigAutoField как тип первичного ключа по умолчанию
Следующее изменение:
Параметр DEFAULT_AUTO_FIELD теперь по умолчанию установлен в BigAutoField.
Это важное обновление, которое помогает сразу использовать масштабируемые первичные ключи большего размера.
В Django 3.2 (2021) появился параметр DEFAULT_AUTO_FIELD, позволяющий задавать тип первичного ключа, который используется по умолчанию в моделях. Django применяет этот параметр, когда автоматически добавляет поле id в модели, где первичный ключ не определён явно. Например, если вы определяете модель так:
from django.db import models
class Video(models.Model):
title = models.TextField()
…то у неё будет два поля: id и title, и тип id будет определяться значением DEFAULT_AUTO_FIELD.
Этот параметр можно переопределить и на уровне отдельного приложения, установив AppConfig.default_auto_field в файле apps.py:
from django.apps import AppConfig
class ChannelConfig(AppConfig):
name = "channel"
default_auto_field = "django.db.models.BigAutoField"
Главным мотивом появления этого параметра была возможность перевести проекты с AutoField (32-битное целое) на BigAutoField (64-битное целое) без необходимости редактировать каждую модель. AutoField хранит значения примерно до 2,1 млрд. Это звучит внушительно, но на больших проектах это ограничение легко достичь. BigAutoField же может хранить значения до примерно 9,2 квинтиллионов, что «заведомо достаточно» для любых практических нагрузок.
Когда модель с AutoField достигает максимального значения, она больше не может принимать новые строки. Это проблема, известная как исчерпание первичного ключа. Таблица фактически перестаёт работать, и требуется срочно переводить модель на BigAutoField, причём для больших таблиц это означает блокирующую миграцию. Отличный разбор того, как Kraken решает эту проблему, можно увидеть в докладе Тима Белла на DjangoCon Europe 2025, где он рассказывает о продвинутых техниках миграции больших таблиц с минимальным простоем.
Чтобы предотвратить такую ситуацию для новых проектов, Django 3.2 сделал так, что новые проекты, созданные через startproject, автоматически задают DEFAULT_AUTO_FIELD равным BigAutoField, а новые приложения из startapp используют BigAutoField в своём AppConfig.default_auto_field. Также появился системный check, заставляющий проекты явно указать DEFAULT_AUTO_FIELD, чтобы разработчики понимали суть параметра и могли осознанно выбрать тип ключа.
Теперь же Django 6.0 меняет фактическое значение настройки и атрибута конфигурации приложения на BigAutoField по умолчанию. Проекты, которые уже используют BigAutoField, могут удалить эту настройку:
-DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
…и атрибут конфигурации приложения:
from django.apps import AppConfig
class ChannelConfig(AppConfig):
name = "channel"
- default_auto_field = "django.db.models.BigAutoField"
Шаблоны startproject и startapp теперь также не задают эти значения. В результате снижается объём шаблонного кода в новых проектах, а проблема исчерпания первичных ключей потихоньку уходит в прошлое, становясь темой, о которой большинству разработчиков Django больше не нужно задумываться.
История
Появление DEFAULT_AUTO_FIELD в Django 3.2 было предложено Кайо Ариеде и реализовано Томом Форбсом в тикете №31007. Новое изменение в Django 6.0 предложил и реализовал бывший Fellow Тим Грэм в тикете №36564. Спасибо Тиму за то, что заметил возможность этой очистки, а также Jacob Walls и Clifford Gama за ревью!
Переменная шаблона forloop.length
Переходя к шаблонам, начнём с небольшой, но приятной добавки:
Внутри цикла for теперь доступна новая переменная forloop.length.
Эта маленькая доработка позволяет писать такие циклы в шаблонах:
<ul>
{% for goose in geese %}
<li>
<strong>{{ forloop.counter }}/{{ forloop.length }}</strong>: {{ goose.name }}
</li>
{% endfor %}
</ul>
Ранее приходилось получать длину иным способом, например через {{ geese|length }}, что менее гибко.
Спасибо Йонатану Штребеле за идею и реализацию в тикете №36186, а также David Smith, Paolo Melchiorre и Sarah Boyce за ревью.
Улучшения тега шаблонов querystring
Есть два расширения для тега querystring, который появился в Django 5.1 и помогает строить ссылки, изменяющие набор query-параметров текущего запроса.
1. Из заметок к релизу:
Тег querystring теперь всегда добавляет префикс ? к возвращаемой строке запроса, обеспечивая предсказуемое формирование ссылок.
Это небольшое изменение улучшает поведение тега, когда передаётся пустой набор параметров. Допустим, у вас есть шаблон:
<a href="{% querystring params %}">Reset search</a>
…где params — словарь, который иногда может быть пустым. Раньше, если params был пуст, результат был таким:
<a href="">Reset search</a>
Браузеры трактуют такую ссылку как переход на тот же URL с теми же query-параметрами, поэтому параметры не очищались. Теперь вывод будет таким:
<a href="?">Reset search</a>
Браузеры понимают ? как ссылку на тот же URL, но без query-параметров, то есть они очищаются так, как ожидает пользователь.
Спасибо Django Fellow Саре Бойс за то, что заметила это улучшение и реализовала фикс в тикете №36268, а также Django Fellow Наталии Бидарт за ревью!
2. Ещё одна заметка к релизу:
Тег querystring теперь принимает несколько позиционных аргументов, которые должны быть отображениями, например QueryDict или dict.
Это расширение позволяет объединять несколько источников query-параметров при формировании выходной строки. Например, может быть такой шаблон:
<a href="{% querystring request.GET super_search_params %}">Super search</a>
…где super_search_params — словарь дополнительных параметров, делающих текущий поиск «супер». Тег объединяет эти отображения, причём более поздние имеют приоритет при одинаковых ключах.
Спасибо снова Саре Бойс за предложение улучшения в тикете №35529, Джаннису Терзопулосу за реализацию и Наталии Бидарт, Саре Бойс и Тому Каррику за ревью!
Русскоязычное сообщество про Python

Друзья! Эту статью подготовила команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!
zo0Mx
На самый главный вопрос современности так и нет ответа - Django ORM когда-нибудь async увидит или ментейнеры решили, что это не особо востребованная фича и можно её отложить в долгий ящик и вспомнить о ней когда-нибудь на закате Django, а то и самого Python-а?
desu7
разработчики джанго просто считают что js должен уйти, поэтому обновление шаблонов намного важнее для этой цели чем какие то богопротивные асинхронщины с их гонками состояний.
whoisking
Давно уже
https://docs.djangoproject.com/en/6.0/topics/async/#queries-the-orm