Web Development · Frameworks
Django 6.0 to 6.1 Upgrade Guide: What Breaks in 2026
Upgrading Django 6.0 to 6.1? The database floors, the first()/last() change, mixed-case SQL aliases and email deprecations that break real projects.
Abhishek Gupta
7 min read
Sponsored
Upgrading from Django 6.0 to 6.1 takes an afternoon on a small project and a sprint on a large one, and the time goes to the same places: your database version, a handful of queryset behavior changes, and the email deprecations. The headline features (fetch modes, database-level deletes) are optional. The breaking changes are not.
Django 6.0 left mainstream support on August 4, 2026. It now gets security and data-loss fixes only, through April 2027, according to the Django downloads page. That gives you months, which is the right amount of time to do this carefully rather than on a Friday. This guide covers what the 6.1 release notes say will break, in the order you are likely to hit it. For what the new features do, see our Django 6.1 feature overview.
Check your infrastructure before touching code
The first blocker is rarely Python code. It is the database server.
| Component | Django 6.1 requires |
|---|---|
| Python | 3.12, 3.13, 3.14 (3.15 as of 6.1.3) |
| PostgreSQL | 15 or higher (14 dropped) |
| MySQL | 8.4 or higher |
| MariaDB | 10.11 or higher |
| SQLite | 3.37.0 or higher (was 3.31.0) |
| GIS | PostGIS 3.1, GEOS 3.8/3.9 and GDAL 3.1/3.2 dropped |
PostgreSQL 14 is the one that bites. It reached end of life this month, and if you are still on it you have two upgrades to schedule, not one. We wrote a separate PostgreSQL 14 end-of-life upgrade guide for that half. Do the database first, on Django 6.0, so that a failure points at one change instead of two.
SQLite matters more than people expect, because CI images and slim Docker bases often ship an older library. Run python -c "import sqlite3; print(sqlite3.sqlite_version)" in the same image your tests use.
Turn on warnings while still on 6.0
Django’s upgrade guide is blunt about this: Python hides deprecation warnings by default, so you have to ask for them.
python -Wa manage.py test
# pytest swallows warnings when output is captured:
PYTHONWARNINGS=always pytest tests --capture=no
Run that on 6.0 and fix what appears. Anything removed in 6.1 was deprecated in 5.2, so a clean 6.0 run catches the removals before they become errors. Those removals are small: the all argument of staticfiles.finders.find() (use find_all), the ordering keyword on ArrayAgg, JSONBAgg and StringAgg (use order_by), and the request.user fallback in login() when user is None.
Warnings from third-party packages are not necessarily your problem. Packages often call deprecated APIs on purpose to support several Django versions.
The four changes that fail quietly
A crash on import is easy. These four give wrong results or break at runtime in one code path.
first() and last() after order_by()
If you clear ordering with a bare order_by(), first() and last() no longer fall back to ordering by primary key.
# 6.0: returns the lowest pk. 6.1: returns whatever the database hands back first.
Order.objects.order_by().first()
Search for order_by() with no arguments next to .first() or .last(). If you wanted determinism, say so: order_by("pk").first().
SQL aliases are always quoted
Annotations, table aliases and joins are now quoted everywhere. That is correct and mostly invisible, until you have RawSQL that refers to a mixed-case alias unquoted. On PostgreSQL an unquoted OrderTotal folds to lowercase and no longer matches. Grep for RawSQL( and .extra(, and quote the alias in your SQL.
union(), difference() and intersection() with default ordering
If the model’s Meta.ordering names a field that your values() or values_list() call did not select, combined queries now raise DatabaseError. The fix from the release notes is to clear the default ordering after combining:
a = Product.objects.filter(active=True).values_list("sku")
b = Product.objects.filter(featured=True).values_list("sku")
skus = a.union(b).order_by()
Strict Base64 parsing
MultiPartParser and DatabaseCache now validate Base64 strictly. Invalid data in an upload raises MultiPartParserError. More awkward: corrupted entries already sitting in a database cache table may become unreadable. If you use DatabaseCache, the cheapest safe move is to clear that table during the deploy.
Smaller breaks worth a grep
These show up less often but are cheap to check.
iexact=Noneon aJSONFieldkey transform now matches JSONnull. It used to behave likeisnull.- A top-level
Nonein anArrayFieldwhose base field is aJSONFieldis stored as SQLNULL, not JSONnull. - Under ASGI,
RemoteUserMiddlewareno longer prefixesHTTP_for custom headers. If you authenticate on a header namedAuthUser, setheadertoHTTP_AUTHUSER. - The
checkcommand now checks every configured database when none is named, and checks may query those databases. A CI step that runsmanage.py checkwithout database access can start failing. SIGNED_COOKIE_LEGACY_SALT_FALLBACKnow defaults toFalse. Sessions signed with the legacy salt stop validating, which logs those users out once.Fileis always truthy now.FieldFileand the upload classes still depend onname.- Custom admin templates: the
wideCSS class is gone,object-toolsmoved out of thecontentblock, and breadcrumbs are now an<ol>of<li>elements.
Clear the cache on deploy
Cache keys for pages and template fragments that vary on extra arguments changed. The first request to each such item after the upgrade is a miss. That is harmless on a small site. On one with a heavy cached page behind a slow query, it is a cold-start spike, so deploy off-peak or warm the important URLs. The upgrade guide also says to clear the cache after upgrading, since pickled objects may not survive across versions.
Email: deprecated, with one hard break
This is the largest surface area and the least urgent. Eleven EMAIL_* settings are deprecated in favor of a single MAILERS dictionary, and they keep working. The migration guide maps them one to one:
MAILERS = {
"default": {
"BACKEND": "django.core.mail.backends.smtp.EmailBackend",
"OPTIONS": {
"host": "mail.example.net",
"use_tls": True,
"username": "user@example.net",
"password": "password",
},
},
}
Note username, not user. Where you used to build a connection by hand, pass using="alias" to send_mail() instead.
The hard break is narrower. Passing fail_silently=True, auth_user or auth_password to a mail function together with an explicit connection now raises TypeError. And EmailMessage.message() raises ValueError if Bcc is in headers; use the bcc argument. Custom backends need a look: consume your keyword arguments before calling super().__init__(), and stop passing fail_silently upward. Django also substitutes a memory backend during tests, so a green suite does not prove your production email path works. Send one real message from staging.
New deprecations to schedule, not rush
None of these fail on 6.1, but they will on a later release. Django also moved to calendar versioning, so RemovedInDjango70Warning is now RemovedInDjango2028Warning, and the removals land in 2028.
select_related()with no arguments: name the relations, or use theFETCH_PEERSfetch mode.values_list(flat=True)with no field: writevalues_list("pk", flat=True).{{ book..title }}double-dot template lookups.salted_hmac()andbase64_hmac()default to"sha1"today and move to"sha256"in 2028. Passalgorithmexplicitly so a future default change does not invalidate signed values.transaction.savepoint()in favor ofsavepoint_create().ModelAdmin.list_select_related = True.
A sequence that keeps the diff reviewable
- Upgrade the database server and Python on Django 6.0. Ship it.
- Move to the latest 6.0 patch, run
python -Wa manage.py test, and fix warnings. Ship it. - Bump to 6.1 in a branch. Fix the four quiet failures and grep the smaller list.
- Deploy off-peak, clear the cache, send one real email, watch error rates.
- Schedule the
MAILERSmigration and the remaining deprecations as their own ticket.
Separate deploys cost little and turn a vague “the upgrade broke something” into a one-line diff. If your team is stretched and the backlog keeps winning, an outside engineer can run steps 1 to 3 as a bounded task; that is the kind of scoped work in our services. Either way, stay off 6.0 before April 2027 and you never face a forced upgrade.
Frequently asked questions
- Can I skip Django 6.1 and wait for the next release?
- You can stay on 6.0 until its extended support ends in April 2027, but only security and data-loss fixes land there. 6.1 is supported for mainstream fixes until April 2027 and extended fixes until December 2027. Django's own upgrade guide recommends moving one feature release at a time, so skipping makes the next jump bigger.
- Is Django 6.1 an LTS release?
- No. Per the Django downloads page, 5.2 is the current LTS, with extended support to April 2028. Teams that want the longest support window with the fewest upgrades can stay on 5.2 and move to 6.x when the next LTS arrives.
- Will my EMAIL_HOST settings stop working in 6.1?
- No. The EMAIL_* settings are deprecated in 6.1 but still work. What changes is that passing fail_silently, auth_user or auth_password together with an explicit connection now raises TypeError, and custom email backends that swallow unknown keyword arguments emit a warning.
Sources
Sponsored
More from this category
More from Web Development
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored