Skip to content

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

Abhishek Gupta

7 min read

A pale concrete footbridge with a glass railing spanning a dark gap between an old mossy stone wall and a new smooth concrete platform

Sponsored

Share

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.

ComponentDjango 6.1 requires
Python3.12, 3.13, 3.14 (3.15 as of 6.1.3)
PostgreSQL15 or higher (14 dropped)
MySQL8.4 or higher
MariaDB10.11 or higher
SQLite3.37.0 or higher (was 3.31.0)
GISPostGIS 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=None on a JSONField key transform now matches JSON null. It used to behave like isnull.
  • A top-level None in an ArrayField whose base field is a JSONField is stored as SQL NULL, not JSON null.
  • Under ASGI, RemoteUserMiddleware no longer prefixes HTTP_ for custom headers. If you authenticate on a header named AuthUser, set header to HTTP_AUTHUSER.
  • The check command now checks every configured database when none is named, and checks may query those databases. A CI step that runs manage.py check without database access can start failing.
  • SIGNED_COOKIE_LEGACY_SALT_FALLBACK now defaults to False. Sessions signed with the legacy salt stop validating, which logs those users out once.
  • File is always truthy now. FieldFile and the upload classes still depend on name.
  • Custom admin templates: the wide CSS class is gone, object-tools moved out of the content block, 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 the FETCH_PEERS fetch mode.
  • values_list(flat=True) with no field: write values_list("pk", flat=True).
  • {{ book..title }} double-dot template lookups.
  • salted_hmac() and base64_hmac() default to "sha1" today and move to "sha256" in 2028. Pass algorithm explicitly so a future default change does not invalidate signed values.
  • transaction.savepoint() in favor of savepoint_create().
  • ModelAdmin.list_select_related = True.

A sequence that keeps the diff reviewable

  1. Upgrade the database server and Python on Django 6.0. Ship it.
  2. Move to the latest 6.0 patch, run python -Wa manage.py test, and fix warnings. Ship it.
  3. Bump to 6.1 in a branch. Fix the four quiet failures and grep the smaller list.
  4. Deploy off-peak, clear the cache, send one real email, watch error rates.
  5. Schedule the MAILERS migration 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

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored