What’s Changed & Upgrading
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Django 6.1 (6.1.1 on PyPI, released 2026-09-02; 6.1.2 is a security release scheduled for
2026-10-06), Python 3.12-3.14 and PostgreSQL 15 or newer, all checked on 2026-10-03 against the release notes of every
version from 3.0 to 6.1, the deprecation timeline and the download page. Where the 5.2 LTS series behaves differently it
is said so. Every listing was run in a scratch project on Python 3.13: the "one model, ten releases" example and an
upgraded legacy app under Django 6.1.1 with manage.py check and the test suite (also under -W error and pytest with
warnings as errors), the django-upgrade diffs with django-upgrade 1.33.0 (2026-10-02), and the release-by-release hop
under Django 5.2.17, 6.0.8 and 6.1.1 side by side. All the books this section draws on were written for Django 1.11 to
2.1, so a large part of what they print no longer runs; this page is where every one of those statements is collected,
with the version in which it changed and the page of this section that shows the current way. The other pages cover each
topic in depth; this one is the map you read before an upgrade and the checklist you keep beside a book.
In a hurry: what goes where
| Need | Rule of thumb | Section |
|---|---|---|
Know which versions are alive |
5.2 LTS (until April 2028), 6.0 (until April 2027) and 6.1 (until December 2027); next are 6.2 LTS in April 2027 and Django 2028 in January 2028. |
|
See what a release brought |
One subsection per feature release from 3.0 to 6.1: date, Python, headline features, breaking changes, removals. |
|
Find when something went away |
The deprecation and removal table, then the official timeline for the full list. |
Deprecations and removals that matter most, The deprecation timeline |
Upgrade a project |
Fix warnings on the current version, move one feature release at a time on its latest patch, run the tests with warnings turned on, deploy, clear the cache. |
|
Rewrite old code automatically |
|
|
Read an old book or tutorial safely |
Look the statement up in the consolidated table; it names the version that changed it and the page with the current form. |
Supported versions and the release policy
Django ships a feature release roughly every eight months and patch releases as needed; every third feature release (x.2) is a long-term support (LTS) release that receives security and data-loss fixes for about three years. A feature deprecated in one release keeps working, with a warning, for at least two feature releases and is then removed. The mechanics of this policy, and why it gives you a predictable upgrade path, are explained in Introduction & Architecture: release policy and its deprecation policy; this section only records the state on 2026-10-03.
| Series | Latest patch | End of mainstream support | End of extended support | Python |
|---|---|---|---|---|
5.2 LTS |
5.2.17 |
2025-12-03 |
April 2028 |
3.10-3.14 (3.14 from 5.2.8) |
6.0 |
6.0.8 |
2026-08-04 |
April 2027 |
3.12-3.14 |
6.1 |
6.1.1 |
April 2027 |
December 2027 |
3.12-3.14 |
Everything older, including 4.2 LTS (extended support ended in April 2026) and 3.2 LTS, receives no more security fixes. The next three releases are already scheduled:
| Series | Release | End of mainstream support | End of extended support |
|---|---|---|---|
6.2 LTS |
April 2027 |
December 2027 |
April 2030 |
Django 2028 |
January 2028 |
January 2029 |
January 2031 |
Django 2029 |
January 2029 |
January 2030 |
January 2032 |
You can check which version a project actually runs from code or from the shell:
$ python -m django --version
6.1.1
$ python -c "import django; print(django.VERSION)"
(6, 1, 1, 'final', 0)
Calendar versioning from Django 2028
Following DEP 20, adopted in Django 6.1, the releases previously planned as 7.0 and 7.1 are called Django 2028 and
Django 2029. From 2028 a feature release ships every January and is named after its year; 6.2 is the last release under
the old numbering and support scheme, and from Django 2028 on every release is supported for three years. The
deprecation warning classes are renamed to the calendar version in which the feature disappears: what 6.0 and 6.1
deprecate is announced by RemovedInDjango2028Warning (formerly RemovedInDjango70Warning).
The rename has a timing detail worth knowing: Django 6.1.1 (2026-09-02) still defines only RemovedInDjango70Warning.
The rename was committed to the 6.1 stable branch on 2026-09-03 and keeps RemovedInDjango70Warning as an alias, so it
reaches users with the next 6.1 patch release. Code that filters warnings should therefore match on the parent class
(PendingDeprecationWarning or DeprecationWarning) rather than on either name, as the pytest configuration in
Turning deprecation warnings on does.
$ python -c "from django.utils import deprecation as d; print([n for n in dir(d) if n.startswith('Removed')])"
['RemovedAfterNextVersionWarning', 'RemovedInDjango70Warning', 'RemovedInNextVersionWarning']
Release by release: 3.0 to 6.1
The books stop at Django 2.1. The table gives the eleven feature releases since then at a glance; each subsection below lists what a project upgrading through that release has to know: the supported Python versions, the headline features, the backwards-incompatible changes and what was removed. Patch releases (x.y.1, x.y.2, …) never add features and are backwards compatible except where a security or data-loss fix requires otherwise, so they are not listed.
| Release | Date | Python | One-line summary |
|---|---|---|---|
3.0 |
2019-12-02 |
3.6-3.9 |
ASGI, |
3.1 |
2020-08-04 |
3.6-3.9 |
Async views and middleware, |
3.2 LTS |
2021-04-06 |
3.6-3.10 |
|
4.0 |
2021-12-07 |
3.8-3.10 |
|
4.1 |
2022-08-03 |
3.8-3.11 |
Async ORM interface and async CBV handlers, constraint validation, |
4.2 LTS |
2023-04-03 |
3.8-3.12 |
psycopg 3, |
5.0 |
2023-12-04 |
3.10-3.12 |
Admin facets, field group templates, |
5.1 |
2024-08-07 |
3.10-3.13 |
|
5.2 LTS |
2025-04-02 |
3.10-3.14 |
Composite primary keys, automatic model imports in |
6.0 |
2025-12-03 |
3.12-3.14 |
Built-in CSP, template partials, the Tasks framework, the modern email API; |
6.1 |
2026-08-05 |
3.12-3.14 |
Fetch modes, database-level |
The Python column gives the full range of the series, including versions added in a later patch (for example 3.14 support arrived in 5.2.8).
Django 3.0 (2019-12-02)
-
Python: 3.6, 3.7, 3.8 and, from 3.0.11, 3.9. Databases: PostgreSQL 9.5 or newer; MariaDB 10.1+ becomes officially supported.
-
Headline features: ASGI support, so Django can run under an async server (the ORM stays synchronous and raises
SynchronousOnlyOperationwhen called from an event loop); enumeration typesTextChoicesandIntegerChoicesforchoices; PostgreSQLExclusionConstraint; boolean expressions directly infilter(). -
Changed defaults:
X_FRAME_OPTIONS = "DENY"andSECURE_CONTENT_TYPE_NOSNIFF = True;FILE_UPLOAD_PERMISSIONSdefaults to0o644. -
Removed: the private Python 2 compatibility layer (
django.utils.six,python_2_unicode_compatible()and friends),render_to_response(), the{% load staticfiles %}andadmin_statictemplate tag libraries. -
Deprecated:
ugettext*()andungettext*(),force_text()andsmart_text(),urlquote()and friends (all removed in 4.0). -
Pages: Async Support, Models & Fields, Security.
Source: Django 3.0 release notes
Django 3.1 (2020-08-04)
-
Python: 3.6-3.9. Databases: MariaDB 10.2 or newer.
-
Headline features: asynchronous views, middleware and test client;
models.JSONFieldon every supported backend; thestartprojecttemplate builds paths withpathlib(BASE_DIR / "templates"); the admin navigation sidebar;PositiveBigIntegerField;on_delete=RESTRICT;HttpRequest.accepts();{% translate %}and{% blocktranslate %}as the new names of{% trans %}/{% blocktrans %}(the old names still work). -
Deprecated:
django.conf.urls.url(),HttpRequest.is_ajax(),NullBooleanField, the PostgreSQL-onlyJSONField,PASSWORD_RESET_TIMEOUT_DAYS,{% ifequal %}/{% ifnotequal %}(all removed in 4.0). -
Pages: Async Support, Models & Fields, Settings & Configuration.
Source: Django 3.1 release notes
Django 3.2 LTS (2021-04-06)
-
Python: 3.6-3.10 (3.10 from 3.2.9). Databases: PostgreSQL 9.6+, MySQL 5.7+.
-
Headline features: the
DEFAULT_AUTO_FIELDsetting andAppConfig.default_auto_field(new projects and apps are generated withBigAutoField, but the global default staysAutoFielduntil 6.0, which is why older projects show themodels.W042warning); automaticAppConfigdiscovery, sodefault_app_configis no longer needed; functional indexes (Index(Lower("title"))); thePyMemcacheCachebackend; the@admin.displayand@admin.actiondecorators;QuerySet.alias();TestCase.captureOnCommitCallbacks(). -
Deprecated:
MemcachedCache(removed in 4.1) anddefault_app_config(removed in 4.1). -
Pages: Models & Fields, Constraints, Indexes & Composite Keys, Admin Customization, Caching.
Source: Django 3.2 release notes
Django 4.0 (2021-12-07)
-
Python: 3.8-3.10. Databases: PostgreSQL 10+, psycopg2 2.8.4+.
-
Headline features:
zoneinfois the default time zone implementation (pytz deprecated); functional unique constraints; the scrypt password hasher; the built-in Redis cache backend; template-based form rendering; the async cache API; CSRF protection checks theOriginheader; the Cross-Origin-Opener-Policy header inSecurityMiddleware. -
Backwards-incompatible: values in
CSRF_TRUSTED_ORIGINSmust include the scheme (https://bookshelf.example);DeleteViewnow usesFormMixin, so custom deletion logic moves fromdelete()toform_valid();USE_L10Ndefaults toTrue. -
Removed:
django.conf.urls.url(),ugettext()/ugettext_lazy()and the otherualiases,force_text()/smart_text(),HttpRequest.is_ajax(),NullBooleanField(except in historical migrations),PASSWORD_RESET_TIMEOUT_DAYS, thedjango-admin.pyentry point. -
Deprecated:
USE_L10N, pytz support (USE_DEPRECATED_PYTZ), and an announcement thatUSE_TZwill default toTrue. -
Pages: Localization & Time Zones, Caching, Form Rendering & Widgets, URLs & Routing, Passwords.
Source: Django 4.0 release notes
Django 4.1 (2022-08-03)
-
Python: 3.8-3.11 (3.11 from 4.1.3). Databases: PostgreSQL 11+, MariaDB 10.3+.
-
Headline features: async handler methods on class-based views; the asynchronous ORM interface (
aget(),acreate(),async forover a queryset);Model.full_clean()validatesMeta.constraints; the accessible<div>-based form template andForm.as_div();bulk_create()can update rows on conflict;Qobjects support XOR; persistent connections getCONN_HEALTH_CHECKS;SECRET_KEY_FALLBACKSallows rotating the secret key. -
Deprecated: logging out with a GET request to
LogoutView(removed in 5.0),CSRF_COOKIE_MASKED,timezone.utc,PickleSerializer. -
Removed:
MemcachedCache,default_app_config. -
Pages: Async Support, Making Queries, Form Rendering & Widgets, Settings & Configuration.
Source: Django 4.1 release notes
Django 4.2 LTS (2023-04-03)
-
Python: 3.8-3.12 (3.12 from 4.2.8). Databases: PostgreSQL 12+, MySQL 8+, MariaDB 10.4+.
-
Headline features: psycopg 3 support (psycopg2 still works); comments on columns and tables (
db_comment,db_table_comment); theSTORAGESsetting andInMemoryStorage;StreamingHttpResponseaccepts async iterators under ASGI; more async model and related-manager methods (asave(),adelete(),aadd(), …); BREACH mitigation inGZipMiddleware; the admin light/dark theme toggle;headers=on the test client. -
Deprecated:
Meta.index_together,DEFAULT_FILE_STORAGEandSTATICFILES_STORAGE, thelength_isfilter, the SHA1 and unsalted MD5 hashers, the PostgreSQL case-insensitive fields,make_random_password()(all removed in 5.1). -
Pages: PostgreSQL & Database Backends, File Uploads & Storage, Static Files & Front-End Assets, Responses, Streaming, CSV & PDF.
Source: Django 4.2 release notes
Django 5.0 (2023-12-04)
-
Python: 3.10, 3.11, 3.12 (Python 3.8 and 3.9 dropped). Databases: MySQL 8.0.11+, SQLite 3.27+.
-
Headline features: facet counts in the admin list filters; field group templates (
{{ form.title.as_field_group }});Field.db_default;GeneratedField;choicesaccepts mappings, callables and enumeration types directly (no.choicessuffix);aauthenticate()/alogin()/alogout();Signal.asend(); async-compatible view decorators;update_or_create(create_defaults=…). -
Backwards-incompatible:
USE_TZdefaults toTrue; the default form and formset rendering style is the<div>one. -
Removed:
USE_L10N(localization is always on), pytz support andis_dst, logout via GET,PickleSerializer,GeoModelAdmin/OSMGeoAdmin. -
Deprecated:
DjangoDivFormRenderer/Jinja2DivFormRenderer,cx_Oracle(useoracledb), thehttpdefault scheme offorms.URLField(all removed in 6.0). -
Pages: Admin Customization, Models & Fields, Authentication, Signals, Localization & Time Zones.
Source: Django 5.0 release notes
Django 5.1 (2024-08-07)
-
Python: 3.10-3.13 (3.13 from 5.1.3). Databases: PostgreSQL 13+, MariaDB 10.5+, SQLite 3.31+.
-
Headline features: the
{% querystring %}template tag; PostgreSQL connection pools (OPTIONS["pool"], psycopg 3 only);LoginRequiredMiddlewarewith@login_not_required; the async session API;__lookups inlist_display;AdminUserCreationForm; SQLiteinit_commandandtransaction_modeoptions. -
Deprecated:
CheckConstraint(check=…)in favour ofcondition=, positional arguments toModel.save()(both removed in 6.0). -
Removed:
Meta.index_together,DEFAULT_FILE_STORAGE/STATICFILES_STORAGE,get_storage_class(),length_is, the SHA1/unsalted hashers, the PostgreSQL CI fields,assertQuerysetEqual(),assertFormsetError(),make_random_password(). -
Pages: Templates, PostgreSQL & Database Backends, Permissions & Authorization, Constraints, Indexes & Composite Keys.
Source: Django 5.1 release notes
Django 5.2 LTS (2025-04-02)
-
Python: 3.10-3.14 (3.14 from 5.2.8); the last series that supports 3.10 and 3.11. Databases: PostgreSQL 14+.
-
Headline features: composite primary keys (
models.CompositePrimaryKey);manage.py shellimports every model automatically; overridingBoundFieldper form or field (bound_field_class);ColorInput,SearchInputandTelInputwidgets; async user-manager and permission helpers (acreate_user(),ahas_perm(), …);reverse()acceptsqueryandfragment; MySQL connections default toutf8mb4. -
Deprecated: the
allargument of the staticfilesfind(), theorderingargument of the PostgreSQL aggregates, thelogin()fallback torequest.user(all removed in 6.1). -
Removed: nothing; as an LTS release 5.2 removes no deprecated feature, which is what makes it a safe stopping point.
-
Pages: Constraints, Indexes & Composite Keys, Management Commands & Shell, Form Rendering & Widgets.
Source: Django 5.2 release notes
Django 6.0 (2025-12-03)
-
Python: 3.12, 3.13, 3.14 (Python 3.10 and 3.11 dropped). Databases: MariaDB 10.6+.
-
Headline features: built-in Content Security Policy (
ContentSecurityPolicyMiddleware,SECURE_CSP,SECURE_CSP_REPORT_ONLY, thecsp()context processor); template partials ({% partialdef %},{% partial %},"template.html#name"); the background Tasks frameworkdjango.tasks(@task,enqueue(), theTASKSsetting; no worker included); email built on Python’s modernemail.message.EmailMessageAPI;AsyncPaginator;forloop.length;StringAggandAnyValueon every backend;shellalso imports common utilities. -
Backwards-incompatible:
DEFAULT_AUTO_FIELDnow defaults toBigAutoField, so projects that never set it get migrations for every implicit primary key unless they pinAutoField; custom expressions'as_sql()must return parameters as a tuple. -
Removed:
cx_Oraclesupport, the transitional div form renderers,CheckConstraint.check, positional arguments toModel.save(),django.utils.itercompat, theFORMS_URLFIELD_ASSUME_HTTPSsetting. -
Deprecated (removed in Django 2028): positional optional arguments of the mail functions,
ADMINS/MANAGERSas tuples,BadHeaderError,SafeMIMEText/SafeMIMEMultipart, the PostgreSQLStringAgg,URLIZE_ASSUME_HTTPS. -
Pages: Content Security Policy, Template Partials & htmx, Tasks Framework, Email, Pagination.
Source: Django 6.0 release notes
Django 6.1 (2026-08-05)
-
Python: 3.12, 3.13, 3.14. Databases: PostgreSQL 15+, MySQL 8.4+, MariaDB 10.11+, SQLite 3.37+; PostGIS 3.1 and older GEOS/GDAL versions dropped.
-
Headline features: model field fetch modes (
FETCH_ONE, the default;FETCH_PEERS, which loads a deferred field or relation for every instance fetched together and so removes most N+1 queries;FETCH_RAISE); database-levelon_delete(DB_CASCADE,DB_SET_NULL,DB_SET_DEFAULT); theMAILERSsetting for several named email backends; theUUID4andUUID7database functions;JSONNull; CSP nonces in the admin and{% csp_nonce_attr %}; PBKDF2 at 1,500,000 iterations;RedirectView.preserve_request; task decorators accept backend-specific keyword arguments. -
Calendar versioning: the next major releases are Django 2028 and 2029 (see Calendar versioning from Django 2028).
-
Removed: the staticfiles
find(all=…)argument, the PostgreSQL aggregates'orderingargument, thelogin()fallback torequest.user. -
Deprecated (removed in Django 2028): the
EMAIL_*settings andget_connection()in favour ofMAILERS;select_related()with no arguments;list_select_related = True;values_list(flat=True)without a field;BLANK_CHOICE_DASH;transaction.savepoint()(renamedsavepoint_create()); the PostgreSQL bit aggregates. -
Pages: Query Optimization & Fetch Modes, Relationships, Email, Passwords.
Source: Django 6.1 release notes
One model, ten releases
The quickest way to feel how much the model layer moved since the books is to write one Bookshelf model with a feature from almost every release. Each comment names the release that introduced the line; none of it existed in Django 2.1.
from decimal import Decimal
from django.db import models
from django.db.models import F, Q
from django.db.models.functions import Lower, Now
class Book(models.Model):
class Format(models.TextChoices): # 3.0: enumeration types for choices
HARDCOVER = "HC", "Hardcover"
EBOOK = "EB", "E-book"
title = models.CharField(max_length=200)
format = models.CharField(max_length=2, choices=Format, default=Format.HARDCOVER) # 5.0: no .choices
metadata = models.JSONField(default=dict, blank=True) # 3.1: JSONField on every backend
pages = models.PositiveIntegerField()
added = models.DateTimeField(db_default=Now()) # 5.0: database-computed default
price = models.DecimalField(max_digits=8, decimal_places=2)
price_with_vat = models.GeneratedField( # 5.0: generated column
expression=F("price") * Decimal("1.21"),
output_field=models.DecimalField(max_digits=10, decimal_places=2),
db_persist=True,
)
class Meta:
indexes = [models.Index(Lower("title"), name="book_title_ci_idx")] # 3.2: functional index
constraints = [
models.UniqueConstraint(Lower("title"), name="book_title_ci_unique"), # 4.0: functional unique
models.CheckConstraint(condition=Q(pages__gt=0), name="book_pages_positive"), # 5.1: condition=
]
def __str__(self):
return self.title
class Review(models.Model): # id is a BigAutoField: DEFAULT_AUTO_FIELD defaults to it since 6.0
book = models.ForeignKey(Book, on_delete=models.DB_CASCADE) # 6.1: database-level ON DELETE
stars = models.PositiveSmallIntegerField()
class ShelfEntry(models.Model): # 5.2: composite primary key
pk = models.CompositePrimaryKey("shelf", "position")
shelf = models.CharField(max_length=50)
position = models.PositiveIntegerField()
book = models.ForeignKey(Book, on_delete=models.DB_CASCADE)
Two details came out of running it. GeneratedField expressions are computed by the database with its own arithmetic:
on SQLite F("price") * 121 / 100 returned 12.00 for a price of 10.00 because the division was done on integers, while
F("price") * Decimal("1.21") returns 12.10. And db_comment (4.2) was left out on purpose: SQLite cannot store column
comments, so manage.py check reports fields.W163 there; on PostgreSQL it is a one-word addition.
The test exercises every comment above and passes under -W error, which turns any deprecation warning into a failure:
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.test import TestCase
from .models import Book, Review, ShelfEntry
class TimelineFeatureTests(TestCase):
def test_features_from_3_0_to_6_1(self):
book = Book.objects.create(title="Dune", pages=412, price=Decimal("10.00"))
book.refresh_from_db()
self.assertEqual(book.price_with_vat, Decimal("12.10")) # GeneratedField (5.0)
self.assertIsNotNone(book.added) # db_default (5.0)
self.assertEqual(Review._meta.pk.get_internal_type(), "BigAutoField") # 6.0 default
with self.assertRaises(ValidationError): # 4.1: full_clean() validates constraints
Book(title="DUNE", pages=1, price=1).full_clean()
entry = ShelfEntry.objects.create(shelf="sci-fi", position=1, book=book)
self.assertEqual(entry.pk, ("sci-fi", 1)) # 5.2: tuple pk
Review.objects.create(book=book, stars=5)
book.delete() # 6.1: the database, not Django, deletes the review and the entry
self.assertFalse(Review.objects.exists() or ShelfEntry.objects.exists())
$ python -W error manage.py makemigrations catalog
Migrations for 'catalog':
catalog/migrations/0001_initial.py
+ Create model Book
+ Create model Review
+ Create model ShelfEntry
$ python -W error manage.py check
System check identified no issues (0 silenced).
$ python -W error manage.py test catalog
Found 1 test(s).
...
Ran 1 test in 0.002s
OK
$ python manage.py sqlmigrate catalog 0001 | grep -o 'ON DELETE CASCADE[^,]*'
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED);
Source: Running tests and sqlmigrate
The last command shows the 6.1 database-level cascade: the ON DELETE CASCADE clause is in the table definition, so the
database removes the reviews and shelf entries and Django does not load them first, which also means no pre_delete
or post_delete signals fire for them. Database and Python variants cannot be mixed (other than DO_NOTHING) in one
model or in models related to each other, which is why both foreign keys to Book use DB_CASCADE. Each feature is explained on its own page: Models & Fields,
Constraints, Indexes & Composite Keys and
Relationships.
Deprecations and removals that matter most
The table lists the deprecations a project written in the books' era is most likely to hit, in the order they were removed. "Deprecated" is the release that started warning; "Removed" is the release in which the old form stops working. The django-upgrade column names the fixer that rewrites the old form automatically (see django-upgrade: rewriting old code automatically); a dash means the change has to be made by hand.
| Old form | Current form | Deprecated | Removed | django-upgrade fixer | Covered on |
|---|---|---|---|---|---|
|
|
1.10 |
2.0 |
- |
|
|
|
1.9 |
2.0 |
- |
|
|
Plain Python 3 ( |
private API, no warning |
3.0 |
- |
|
|
|
2.0 |
3.0 |
|
|
|
|
2.1 |
3.0 |
- |
|
|
|
3.1 |
4.0 |
|
|
|
|
3.0 |
4.0 |
|
|
|
|
3.0 |
4.0 |
|
|
|
|
3.1 |
4.0 |
- |
|
|
|
3.1 |
4.0 |
|
|
|
|
3.1 |
4.0 |
|
|
|
|
3.1 |
4.0 |
|
|
|
|
3.2 |
4.1 |
- |
|
|
Nothing: |
3.2 |
4.1 |
|
|
|
Nothing: localized formatting is always on |
4.0 |
5.0 |
|
|
pytz time zones, |
|
4.0 |
5.0 |
- |
|
Logout with a GET request to |
A POST form with |
4.1 |
5.0 |
- |
|
|
|
4.1 |
5.0 |
|
|
|
|
4.2 |
5.1 |
|
|
|
|
4.2 |
5.1 |
|
|
|
|
4.2 |
5.1 |
|
|
|
|
5.1 |
6.0 |
|
|
Positional arguments to |
Keyword arguments ( |
5.1 |
6.0 |
- |
|
|
|
5.0 |
6.0 |
- |
|
Implicit |
|
warned since 3.2 ( |
default changed in 6.0 |
|
|
|
|
6.1 |
Django 2028 |
|
|
|
|
6.1 |
Django 2028 |
- |
|
|
|
6.1 |
Django 2028 |
|
Source: Django Deprecation Timeline and django-upgrade: Fixers
The deprecation timeline
The official deprecation timeline is organised by the release in which things are removed, newest first, and each block points to the release notes "of two versions prior" where the deprecation was announced. On 2026-10-03 its blocks are 2028, 6.1, 6.0, 5.1, 5.0, 4.1, 4.0, 3.1, 3.0 and older. There are no 4.2, 5.2 or 6.2 blocks: an LTS release removes nothing. Features deprecated in an x.0 or x.1 release are removed in (x+1).0, and features deprecated in the x.2 LTS are removed in (x+1).1 — which is why 5.2 deprecations disappear in 6.1. That is the rule that makes the LTS-to-LTS path predictable.
| Deprecated in | Warning class while it still works | Removed in |
|---|---|---|
5.0 or 5.1 |
|
6.0 |
5.2 LTS |
|
6.1 |
6.0 or 6.1 |
|
Django 2028 |
6.2 LTS |
named after the release that removes it |
Django 2029 |
The warning raised one release before removal subclasses DeprecationWarning; earlier than that it subclasses
PendingDeprecationWarning: Django 5.2.17 defines RemovedInDjango60Warning(DeprecationWarning) and
RemovedInDjango61Warning(PendingDeprecationWarning), Django 6.0.8 RemovedInDjango61Warning(DeprecationWarning) and
RemovedInDjango70Warning(PendingDeprecationWarning). Python never shows PendingDeprecationWarning by default and shows
DeprecationWarning only for code run directly as __main__, so neither appears when manage.py imports your modules.
That is why a project can run for years on deprecated APIs without anyone noticing.
The Django 2028 block is the next one you will meet. Grouped by area, it removes:
-
Email: the
EMAIL_BACKEND,EMAIL_HOST,EMAIL_PORT,EMAIL_HOST_USER,EMAIL_HOST_PASSWORD,EMAIL_USE_TLS,EMAIL_USE_SSL,EMAIL_SSL_CERTFILE,EMAIL_SSL_KEYFILE,EMAIL_TIMEOUTandEMAIL_FILE_PATHsettings;get_connection(); theconnection,fail_silently,auth_userandauth_passwordarguments of the mail functions; positional optional arguments;BadHeaderError,SafeMIMEText/SafeMIMEMultipartand legacyMIMEBaseattachments. -
Settings:
ADMINS/MANAGERSas(name, address)tuples; the transitionalURLIZE_ASSUME_HTTPS,USE_BLANK_CHOICE_DASHandSIGNED_COOKIE_LEGACY_SALT_FALLBACKsettings. -
ORM:
select_related()with no arguments or withTrue;values_list(flat=True)without a field;Nonein aJSONFieldtop-level lookup will mean SQLNULL(useJSONNull()for JSONnull);BLANK_CHOICE_DASH;transaction.savepoint(); percent signs in column aliases; the PostgreSQLStringAgg,OrderableAggMixinand bit aggregates. -
Admin:
list_select_related = True;get_actions()andget_action_choices()overrides without the newaction_locationparameter. -
Templates and utilities: double-dot variable lookups such as
{{ book..title }};urlizedefaults to HTTPS; thesalted_hmac()default algorithm moves from SHA-1 to SHA-256;Paginator(orphans=…)greater than or equal toper_pageis rejected.
Turning deprecation warnings on
The upgrade guide asks you to make the warnings visible before changing anything. With the Django test runner, pass
-Wa (show every occurrence) to Python, as the 6.1 how-to does; the -Wd often quoted in older guides shows each
warning once per code location, which is enough for a first inventory. -W error turns each warning into an exception,
which is what you want in CI once the warnings are fixed:
$ python -Wa manage.py test
$ python -W error manage.py test
$ PYTHONWARNINGS=always pytest --capture=no
With pytest the same policy is better kept in the configuration, so nobody has to remember the flag. Matching the parent classes covers every Django warning class, whatever its name (and the 6.1 rename):
[project]
name = "bookshelf"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["django>=6.1,<6.2"]
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings"
python_files = ["tests.py", "test_*.py"]
filterwarnings = [
"error::DeprecationWarning",
"error::PendingDeprecationWarning",
]
$ pytest -q legacy catalog
.... [100%]
4 passed in 0.29s
Third-party packages sometimes call deprecated APIs on purpose so they can support several Django versions; a warning
raised from inside a package is a reason to check for a newer release or open an issue, not necessarily a bug. If one
blocks error:: mode, add a narrow ignore: entry for that package’s module rather than turning the policy off. The
pytest, Factories & Browser Tests page covers the rest of the
pytest-django set-up.
Upgrading Django
Upgrading regularly keeps the project on a version that still receives security fixes, brings new features and bug fixes, and keeps each individual upgrade small. The official how-to describes the same loop for every hop; the diagram condenses it.
The steps in detail
-
Read. Read the release notes of every feature release after the one you run, up to and including the target, paying most attention to the "Backwards incompatible changes" sections, and look the same releases up in the deprecation timeline. The Release by release: 3.0 to 6.1 above is the short version.
-
Resolve warnings first. On the version you run today, run the test suite with
python -Wa manage.py testand fix everyRemovedInDjango…Warningthat comes from your own code. A warning fixed now is a crash avoided after the upgrade. -
Go one feature release at a time. Upgrading 4.2 to 6.1 means 4.2 → 5.0 → 5.1 → 5.2 → 6.0 → 6.1, each on its latest patch release, rather than all the changes at once. The same incremental approach is recommended between LTS releases: LTS to LTS is a sequence of hops, not one jump. Python moves too: 6.0 needs Python 3.12, so a project on 3.10 or 3.11 upgrades Python while it is on 5.2.
-
Upgrade the dependencies. Most Django-related packages publish a release for each new Django version; if one has not yet, you may have to wait or contribute the fix. PyPI classifiers often lag behind: on 2026-10-03 Channels 4.3.2, drf-spectacular, WhiteNoise and django-tasks still listed Django 6.0 as their newest classifier, while DRF 3.18.1, Django Ninja 1.7.1, django-debug-toolbar 8.0.0 and django-allauth already listed 6.1. A missing classifier is a reason to run the tests, not proof of incompatibility.
-
Install in a fresh environment. For a major upgrade create a new virtual environment, then
python -m pip install -U "Django>=6.1,<6.2"(or bump the pin inpyproject.tomlanduv lock --upgrade-package django). -
Test. Run the whole suite with warnings visible, plus
manage.py check(andcheck --deploywith production settings) andmanage.py makemigrations --check --dry-run, which fails if the upgrade changed a model state, as theDEFAULT_AUTO_FIELDchange in 6.0 does for projects that never set it. -
Deploy and clear the cache. Objects pickled into the cache by one Django version are not guaranteed to unpickle in another (cached
HttpResponseobjects fromcache_page()were a past example), so clear the cache after deploying.
A hop through 5.2 → 6.0 → 6.1, for real
A small app written against Django 5.2 shows what each step catches. It uses two APIs deprecated in 5.1 and never sets
DEFAULT_AUTO_FIELD:
from django.db import models
from django.db.models import Q
class Loan(models.Model):
days = models.PositiveIntegerField()
class Meta:
constraints = [
models.CheckConstraint(check=Q(days__lte=60), name="loan_days_max_60"),
]
# hop/tests.py
from django.test import TestCase
from .models import Loan
class LoanTests(TestCase):
def test_create(self):
loan = Loan(days=14)
loan.save(False, False) # positional force_insert, force_update
self.assertEqual(Loan.objects.count(), 1)
On Django 5.2.17 the plain test run is green and only shows the models.W042 system check warning; the deprecations
appear only with -Wa:
$ python -m django --version
5.2.17
$ python manage.py test
Found 1 test(s).
Creating test database for alias 'default'...
System check identified some issues:
WARNINGS:
hop.Loan: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the AppConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
System check identified 1 issue (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
$ python -Wa manage.py test
hop/models.py:10: RemovedInDjango60Warning: CheckConstraint.check is deprecated in favor of `.condition`.
models.CheckConstraint(check=Q(days__lte=60), name="loan_days_max_60"),
...
hop/tests.py:9: RemovedInDjango60Warning: Passing positional arguments to save() is deprecated
loan.save(False, False) # positional force_insert, force_update
Skipping step 2 and installing Django 6.0.8 straight away turns the first warning into a crash at import time:
$ python -m django --version
6.0.8
$ python manage.py test
...
models.CheckConstraint(check=Q(days__lte=60), name="loan_days_max_60"),
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: CheckConstraint.__init__() got an unexpected keyword argument 'check'
Back on 5.2, django-upgrade --target-version 6.0 fixes the constraint (fixer check_constraint_condition); it has no
fixer for positional save() arguments, so that line is changed by hand. Because the project never set
DEFAULT_AUTO_FIELD, the 6.0 release notes recommend pinning the old type so that 6.0 does not generate a migration for
every table:
--- hop/models.py
+++ hop/models.py
@@ -7,5 +7,5 @@
class Meta:
constraints = [
- models.CheckConstraint(check=Q(days__lte=60), name="loan_days_max_60"),
+ models.CheckConstraint(condition=Q(days__lte=60), name="loan_days_max_60"),
]
# hop/tests.py, by hand
loan.save(force_insert=False, force_update=False)
# settings.py
DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # keep existing integer keys
With the warnings gone the same code passes under -W error on all three versions, so each hop is now a version bump:
$ for py in ../.venv5.2/bin/python ../.venv6.0/bin/python ../.venv/bin/python; do
> $py -m django --version
> $py -W error manage.py test 2>&1 | grep -E "^(OK|FAILED)"
> done
5.2.17
OK
6.0.8
OK
6.1.1
OK
Source: Django 5.1 release notes: Features deprecated in 5.1 and Django 6.0 release notes: DEFAULT_AUTO_FIELD now defaults to BigAutoField
Projects that already set DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" (every project generated by
startproject from 3.2 to 5.2) are unaffected, and on 6.0 they can delete the line, which django-upgrade’s
default_auto_field fixer does. Changing an existing project from AutoField to BigAutoField is possible but is a real
schema migration on every table and every foreign key pointing at it, to be planned on its own, not as a side effect of
the upgrade.
django-upgrade: rewriting old code automatically
django-upgrade, by Django contributor Adam Johnson and listed under
"upgrade utilities" on the official community ecosystem page, is a command-line tool that rewrites Python files so they
stop using deprecated APIs, and in a few cases adopts the newer syntax. It enables every fixer up to and including the
--target-version you give it; without the option it reads the minimum Django version from project.dependencies in
pyproject.toml (falling back to 2.2). It does not recurse into directories, so you hand it the file list, and it exits
with status 1 when it changed something so a CI job or pre-commit run stops for review.
$ python -m pip install django-upgrade
$ django-upgrade --version
1.33.0
$ git ls-files -z -- '*.py' | xargs -0r django-upgrade --target-version 6.1
$ django-upgrade --check legacy/*.py # report only, exit 1 if something would change
$ django-upgrade --list-fixers # names for --only / --skip
Source: django-upgrade: Usage and django-upgrade: Options
The usual way to keep it running is a pre-commit hook placed before formatters such as Black or Ruff, because django-upgrade "focuses on upgrading your code and not on making it look nice":
repos:
- repo: https://github.com/adamchainz/django-upgrade
rev: "1.33.0"
hooks:
- id: django-upgrade
args: [--target-version, "6.1"]
Source: django-upgrade: Installation
A real run on book-era code
The four files below are written the way the books of this section wrote Django code: regex url() routes,
ugettext, force_text, request.is_ajax(), request.META, NullBooleanField, index_together and admin attributes.
Under Django 6.1.1 the project does not even import:
$ python manage.py check
...
ImportError: cannot import name 'ugettext_lazy' from 'django.utils.translation' (.../django/utils/translation/__init__.py)
One command rewrites all four files (the real output and diff of django-upgrade 1.33.0):
$ django-upgrade --target-version 6.1 legacy/*.py
Rewriting legacy/admin.py
Rewriting legacy/models.py
Rewriting legacy/urls.py
Rewriting legacy/views.py
$ echo $?
1
--- legacy/urls.py
+++ legacy/urls.py
@@ -1,12 +1,12 @@
-from django.conf.urls import include, url
+from django.urls import include, path, re_path
from . import views
app_name = "legacy"
urlpatterns = [
- url(r"^$", views.book_list, name="book_list"),
- url(r"^books/(?P<pk>[0-9]+)/$", views.book_detail, name="book_detail"),
- url(r"^books/(?P<slug>[-\w]+)/reviews/$", views.reviews, name="reviews"),
- url(r"^api/", include("legacy.api_urls")),
+ path("", views.book_list, name="book_list"),
+ path("books/<int:pk>/", views.book_detail, name="book_detail"),
+ re_path(r"^books/(?P<slug>[-\w]+)/reviews/$", views.reviews, name="reviews"),
+ path("api/", include("legacy.api_urls")),
]
--- legacy/views.py
+++ legacy/views.py
@@ -1,14 +1,14 @@
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
-from django.utils.encoding import force_text
-from django.utils.translation import ugettext as _
+from django.utils.encoding import force_str
+from django.utils.translation import gettext as _
from .models import Book
def book_list(request):
if request.is_ajax():
- return JsonResponse({"titles": [force_text(b) for b in Book.objects.all()]})
+ return JsonResponse({"titles": [force_str(b) for b in Book.objects.all()]})
return render(request, "legacy/book_list.html", {"title": _("Books")})
@@ -18,5 +18,5 @@
def reviews(request, slug):
- accept = request.META["HTTP_ACCEPT"]
+ accept = request.headers["accept"]
return render(request, "legacy/reviews.html", {"accept": accept})
--- legacy/models.py
+++ legacy/models.py
@@ -1,13 +1,13 @@
from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
class Book(models.Model):
title = models.CharField(_("title"), max_length=200)
- is_published = models.NullBooleanField()
+ is_published = models.BooleanField(null=True)
class Meta:
- index_together = [("title", "is_published")]
+ indexes = [models.Index(fields=("title", "is_published"))]
def __str__(self):
return self.title
--- legacy/admin.py
+++ legacy/admin.py
@@ -3,14 +3,16 @@
from .models import Book
+@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ["title", "is_published", "title_length"]
+ @admin.display(
+ description="Length",
+ ordering="title",
+ )
def title_length(self, obj):
return len(obj.title)
- title_length.short_description = "Length"
- title_length.admin_order_field = "title"
-admin.site.register(Book, BookAdmin)
Source: django-upgrade: Fixers
What it left for you
Reading the diff is part of the job; four things in it need a human:
-
request.is_ajax()is untouched. There is no fixer, because the right replacement depends on intent. The 3.1 release notes give the exact equivalent,request.headers.get("x-requested-with") == "XMLHttpRequest", orrequest.accepts("application/json")if the code really cares about theAcceptheader. Note thataccepts()returnsTruewhen the client sends noAcceptheader at all (the test client sends none), so a test that expected HTML got JSON when the scratch run first tried it. -
[-\w]+stays a regex. Only regexes that exactly match a path converter ([0-9]+,[-a-zA-Z0-9_]+,[^/]+,.+, the UUID pattern) are converted, so theslugroute becamere_path().\wmatches non-Latin letters and Django’sslugconverter does not, so changing it to<slug:slug>is a behaviour change you decide on. Where a route was converted, the view now receives anintinstead of astr; test those views. -
Model changes need migrations. The
null_boolean_fieldandindex_togetherfixers change model state. Runmakemigrations: forindex_togetherit generates aRenameIndexoperation that keeps the existing index. Keepmakemigrations --checkin CI so a forgotten migration fails the build. -
Formatting is not its job. The admin rewrite left three blank lines at the end of the file; run your formatter after it.
@@ -7,7 +7,7 @@
def book_list(request):
- if request.is_ajax():
+ if request.headers.get("x-requested-with") == "XMLHttpRequest":
return JsonResponse({"titles": [force_str(b) for b in Book.objects.all()]})
return render(request, "legacy/book_list.html", {"title": _("Books")})
Source: Django 3.1 release notes: Features deprecated in 3.1 and Django 4.2 release notes: index_together is deprecated
With the follow-up done, three tests exercise the upgraded routes, the AJAX branch and the converter, and the app is
clean under -W error:
from django.test import TestCase
from django.urls import reverse
from .models import Book
class LegacyUpgradeTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.book = Book.objects.create(title="Dune", is_published=None)
def test_list_html_and_json(self):
self.assertContains(self.client.get(reverse("legacy:book_list")), "Books")
response = self.client.get(reverse("legacy:book_list"), headers={"x-requested-with": "XMLHttpRequest"})
self.assertEqual(response.json(), {"titles": ["Dune"]})
def test_detail_uses_int_converter(self):
self.assertEqual(reverse("legacy:book_detail", args=[self.book.pk]), f"/legacy/books/{self.book.pk}/")
def test_ping(self):
self.assertEqual(self.client.get("/legacy/api/ping/").json(), {"ok": True, "titles": ["Dune"]})
$ python manage.py makemigrations legacy
Migrations for 'legacy':
legacy/migrations/0001_initial.py
+ Create model Book
$ python -W error manage.py check
System check identified no issues (0 silenced).
$ python -W error manage.py test legacy
Found 3 test(s).
...
Ran 3 tests in 0.012s
OK
The same run is a good illustration of why warnings must be on. A helper view written in the 6.1 era used
select_related() without arguments, which 6.1 deprecates. The plain test run passed; -Wa printed the warning, and
-W error failed the test until the call was removed:
$ python -Wa manage.py test legacy
legacy/views.py:26: RemovedInDjango70Warning: Calling select_related() with no arguments is deprecated. Specify the fields to fetch instead.
...
OK
$ python -W error manage.py test legacy
...
django.utils.deprecation.RemovedInDjango70Warning: Calling select_related() with no arguments is deprecated. Specify the fields to fetch instead.
...
FAILED (errors=1)
Settings files are handled too, by fixers that only activate for modules with "settings" in their path. On a
book-era settings/base.py it removes USE_L10N = True and DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
(both now defaults) and folds the two storage settings into STORAGES:
-USE_L10N = True
USE_TZ = True
-DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
-DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
-STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
+STORAGES = {
+ "default": {
+ "BACKEND": "django.core.files.storage.FileSystemStorage",
+ },
+ "staticfiles": {
+ "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
+ },
+}
Some old settings have no fixer at all: MIDDLEWARE_CLASSES, pytz
time zones and the EMAIL_* settings beyond get_connection() are converted by hand following
Settings & Configuration,
Localization & Time Zones and
Email; the official mailers
migration guide covers the last one.
Where the books are outdated: the consolidated table
The four reference books of this section (listed in the section bibliography) remain good at explaining why Django works the way it does, and most of their concepts are still current. What has aged is specific API usage, settings, tooling and third-party advice. The tables below collect every such statement found while writing this section, paraphrased (no book text is reproduced), with the chapter where it appears, the version in which it changed and the current form. The topic pages repeat the relevant rows in their own "where the books are outdated" sections with more context.
Book abbreviations used in the tables:
-
M: Antonio Melé, Django 2 by Example (Packt, 2018; Django 2.0), chapters 1-13.
-
TS: Daniel Roy Greenfeld and Audrey Roy Greenfeld, Two Scoops of Django 1.11 (Two Scoops Press, 2017; Django 1.11), chapters and section numbers as printed; "App." is an appendix.
-
Ma: Federico Marani, Practical Django 2 and Channels 2 (Apress, 2019; Django 2.0/2.1, Channels 2.1), chapters 1-10.
-
V: William S. Vincent, Django for Beginners (2018; Django 2.1), chapters 1-15.
Runtime and tooling
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
Django 1.11-2.1 on Python 2.7 or 3.6/3.7 as the target platform |
all four |
Python < 3.12 dropped in Django 6.0 |
Django 6.1 on Python 3.12-3.14, or 5.2 LTS on 3.10-3.14 until April 2028 |
|
Pipenv (presented as the recommended tool) or virtualenvwrapper for environments |
V Intro, ch. 1 onward; Ma ch. 1 onward; TS ch. 2 and 3; M ch. 1 |
ecosystem, not a Django release |
|
|
psycopg2 as the PostgreSQL driver |
M ch. 3; TS ch. 5; Ma ch. 1 |
psycopg 3 supported since 4.2 (and needed for 5.1 connection pools) |
|
|
|
V ch. 3-9; M ch. 13 |
|
|
|
Heroku’s free tier for the first deployments |
V ch. 3, 4, 7; TS ch. 30; Ma ch. 10 |
Heroku stopped free plans on 2022-11-28 |
A paid PaaS plan, a container platform or a VPS; the deployment checklist is the same |
|
Travis CI (and other hosted CI of the time) and Bitbucket as the code host |
TS ch. 32; V ch. 2-7 |
ecosystem |
GitHub Actions or GitLab CI running the test suite with warnings as errors |
Source: FAQ: Python versions, PostgreSQL notes and Heroku: the next chapter
URLs and views
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
|
TS 1.6, 8.2-8.4, ch. 16, ch. 33 |
deprecated 3.1, removed 4.0 |
|
|
|
TS 8.4, 16.3.5 |
removed 2.0 |
|
|
|
M ch. 5 |
deprecated 3.1, removed 4.0 |
|
|
The |
- |
deprecated 2.0, removed 3.0 |
|
|
A plain |
M ch. 4, 10; Ma ch. 7; V ch. 7, 9, 10 |
GET deprecated 4.1, removed 5.0: the link now returns 405 |
A |
|
Custom deletion logic in |
Ma ch. 4; V ch. 6, 14 |
|
Put the logic in |
|
Function-based auth views ( |
TS ch. 20 |
removed 2.1 |
|
Helpers and settings
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
|
TS ch. 1 examples, 29.3.12, App. D; M ch. 9 (prose) |
deprecated 3.0, removed 4.0 |
|
|
|
TS 29.3.4, App. D |
deprecated 3.0, removed 4.0 |
|
|
|
TS Intro, ch. 21, 19.3, 29.3.8, App. D and F |
removed 3.0 |
Plain Python 3: |
|
|
TS ch. 26, App. G |
removed 2.0 |
|
|
|
M ch. 4, 10; TS 13.5; Ma ch. 5 |
removed 3.0 |
|
|
|
M ch. 9 |
deprecated 4.0, removed 5.0 |
Delete it: localized formatting is always on |
|
pytz time zones; |
TS 29.3.11, App. A; all four (settings) |
|
|
|
|
M ch. 9 |
renamed 3.1 (old names still work) |
|
Models
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
Models with an implicit |
all four |
|
Nothing to write for new projects; pin |
|
|
M ch. 5, 7, 9 |
deprecated 4.2, removed 5.1 |
|
|
|
TS 6.4.4 |
deprecated 3.1, removed 4.0 |
|
|
Choices as tuples of module or class constants |
TS 6.4.7-6.4.8; Ma ch. 4 |
enumeration types 3.0; enums passed directly 5.0 |
|
|
The PostgreSQL-only |
TS 6.4.9 |
|
|
|
|
M ch. 1, 6, 10; V ch. 5 |
not removed; recommended practice since 1.5 |
|
|
|
M ch. 6 |
never supported API |
A custom user model, or a one-to-one profile model |
|
Full-text search vectors computed per query; |
M ch. 3 |
|
A |
Files and caching
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
|
V ch. 7; Ma ch. 10 |
|
|
|
|
M ch. 11 |
|
|
Source: STORAGES setting and Cache framework: Redis
Admin and forms
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
|
M ch. 8; TS ch. 19; Ma ch. 3, 7 |
|
|
|
Forms rendered by hand-written loops over fields, or |
Ma ch. 2; TS 12.8; V throughout |
template-based rendering 4.0, |
|
Source: The display decorator and Reusable field group templates
Async and background work
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
Django is synchronous; Channels (or a task queue, or serverless functions) is the only way to do async work |
TS ch. 25, App. I; Ma ch. 8 |
ASGI 3.0, async views 3.1, async ORM 4.1, async CBV handlers 4.1, async auth 5.0, async sessions 5.1 |
Native |
|
SSE and the reverse proxy built as Channels HTTP consumers |
Ma ch. 8 |
async iterators in |
An |
|
Celery (or django-background-tasks) as the only answer for background jobs |
M ch. 7; TS ch. 25 |
|
|
Source: Asynchronous support and Django’s Tasks framework
Security
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
django-csp for a Content Security Policy |
TS 26.10 |
built-in CSP 6.0 |
|
|
Deployment settings that list |
V ch. 3, 4, 7; M ch. 13; Ma ch. 10 |
|
|
|
PBKDF2 iteration counts and hasher advice of 2017 |
TS ch. 26 |
raised every release, 1,500,000 in 6.1; scrypt hasher 4.0 |
Keep the default PBKDF2 hasher or put Argon2/scrypt first in |
|
Login protection for every view by decorating or mixing in one by one |
V ch. 14; TS ch. 10 |
|
Optionally |
Channels 2 to Channels 4
Channels is a separate project with its own releases (3.0 in 2020, 4.0 in 2022, 4.3.2 current on 2026-10-03); its
classifiers stop at Django 6.0, but it installs and passes manage.py check under Django 6.1.1.
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
Routes that point at the consumer class itself |
Ma ch. 8 |
Channels 3 |
|
|
Two-step middleware ( |
Ma ch. 8 |
Channels 3 (ASGI 3) |
A single async callable |
|
|
Ma ch. 8, 10 |
deprecated Channels 3, removed Channels 4 |
|
|
An |
Ma ch. 10 |
Django’s own |
Django’s generated |
|
|
Ma ch. 8 |
Daphne unbundled in Channels 4 |
|
|
|
Ma ch. 8, 10 |
aioredis merged into redis-py 4.2 as |
|
|
Synchronous ORM calls (for example a token lookup) inside async middleware or consumers |
Ma ch. 8 |
|
|
Other third-party packages and Python
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
|
M ch. 7 |
|
|
|
DRF |
M ch. 12 |
deprecated DRF 3.8, removed DRF 3.10 |
|
|
DRF Core API schemas ( |
TS 16.7-16.8 |
deprecated DRF 3.10 (OpenAPI), removed DRF 3.17.0 (2026-03-18) |
OpenAPI 3 with drf-spectacular |
|
|
Ma ch. 6 |
django-filter 2 |
|
|
django-crispy-forms 1.x with |
V ch. 10 |
crispy-forms 2.0 (2023-02-13) moved the Bootstrap packs out |
|
|
Bootstrap 4 with jQuery and Popper from a CDN; jQuery |
V ch. 10; Ma ch. 2, 4; M ch. 5, 6, 10 |
Bootstrap 5 dropped jQuery |
Bootstrap 5 (or none), |
|
|
TS ch. 22 |
renamed |
model_bakery or factory_boy |
|
|
Ma ch. 4, 6, 8; TS 22.3 |
alias removed in Python 3.12 |
|
|
Per-test |
V ch. 4-10 |
|
|
|
Pillow |
Ma ch. 3 |
removed in Pillow 10.0 |
|
Services and deployment
| What the book shows | Where | Changed in | Current form | Page |
|---|---|---|---|---|
Google+ as a social login provider |
M ch. 4 |
Google+ APIs shut down on 2019-03-07 |
Google sign-in through OpenID Connect, for example with django-allauth |
|
Sending mail from Gmail with the account password after enabling "less secure apps" |
M ch. 2 |
Google stopped allowing less secure app sign-in in January 2025 |
An app password or OAuth for Gmail, or a transactional email provider; credentials from the environment; |
|
uWSGI behind NGINX as the default application server |
M ch. 13; TS ch. 31 |
uWSGI is in maintenance mode |
Gunicorn (WSGI), or Uvicorn, Daphne or Granian (ASGI), behind a reverse proxy |
|
|
Ma ch. 10 |
replaced by |
|
Source: https://developers.google.com//api-shutdown[Google API shutdown], Google: Less secure apps and uWSGI documentation
Errata and practices not to copy
Some statements were never correct, whatever the version. They are listed so a reader comparing a book with this section knows why the section does it differently; each page states the correct form where the topic comes up.
| What the book does | Where | Correct practice | Page |
|---|---|---|---|
|
all four |
Read secrets from the environment or a secret manager; generate keys with |
|
|
V ch. 3, 4, 7; M ch. 13; Ma ch. 10 |
Real host names, |
|
State changes through GET (add to basket, logout links) |
Ma ch. 4; all four (logout) |
POST forms with |
|
|
M ch. 10 |
Keep CSRF protection and send the token in the |
|
Fetching a user-supplied URL without validation (server-side request forgery) |
M ch. 5 |
Allow-list schemes and hosts, time out, limit the size, never fetch internal addresses |
|
Markdown rendered and passed through |
M ch. 3; Ma ch. 8 |
Sanitise the HTML (for example with nh3) before marking it safe; |
|
A |
Ma ch. 6; V ch. 6 |
Scope |
|
Orders and payments written without |
M ch. 7-9; Ma ch. 4 |
|
|
Broken listings: a |
M ch. 12-13 |
Run every example; this section’s listings were all executed |
|
Deleted coupon or basket ids left in the session |
M ch. 9; Ma ch. 4 |
Handle |
|
Sync ORM on the event loop, a token in the WebSocket query string, Redis |
Ma ch. 8 |
Async ORM or |
|
Tests that pass for the wrong reason (a POST asserting 200, a delete test that never deletes, |
V ch. 6 |
Assert the redirect and the database state; pass |
|
An ownership check in |
V ch. 14 |
Put |
|
|
M ch. 6; M throughout |
A custom user model; a test for every view and form |
Source: Deployment checklist and Security in Django
New since the books
The other half of "outdated" is what the books could not mention. Each of these is a reason to prefer the current docs over a book chapter on the same topic; the version is the one in which the feature first appeared.
| Feature | Since | Page |
|---|---|---|
ASGI support |
3.0 |
|
Async views, middleware and test client |
3.1 |
|
Async cache API |
4.0 |
|
Async ORM interface, async CBV handlers |
4.1 |
|
Async iterators in |
4.2 |
|
|
5.0 |
|
Async session API |
5.1 |
|
Async user-manager and permission helpers |
5.2 |
|
|
6.0 |
|
|
3.2 |
|
Functional unique constraints |
4.0 |
|
Constraint validation in |
4.1 |
|
psycopg 3 |
4.2 |
|
|
5.0 |
|
PostgreSQL connection pools |
5.1 |
|
Composite primary keys |
5.2 |
|
Fetch modes, database-level |
6.1 |
|
Template-based form rendering |
4.0 |
|
|
4.1 |
|
Field group templates, admin facets |
5.0 |
|
|
5.1 |
|
Template partials |
6.0 |
|
Scrypt hasher |
4.0 |
|
|
4.1 |
|
|
5.1 |
|
Built-in Content Security Policy |
6.0 |
|
Automatic model imports in |
5.2 |
|
|
6.0 |
|
Email on the modern |
6.0 |
|
|
6.1 |
|
Granian deployment how-to (ASGI and WSGI) |
docs for 6.1 |
Source: Release notes index and How to use Django with Granian
Two dates in this table differ from what is often repeated: async iterators in StreamingHttpResponse arrived in 4.2,
not 6.0 (6.0 added AsyncPaginator), and the scrypt hasher arrived in 4.0 (5.1 only raised its default parallelism).
Both were checked in the release notes.
Pitfalls
-
Jumping several releases at once. Each removal is announced by a warning in the two releases before it; skipping releases skips the warnings, and the first sign is an
ImportErrororTypeErrorat start-up, as in A hop through 5.2 → 6.0 → 6.1, for real. -
Running the tests without warnings. Django’s warnings are invisible by default; a green suite under a plain
manage.py testsays nothing about the next release. Use-Walocally and warnings-as-errors in CI. -
Letting 6.0 change your primary keys. A project that never set
DEFAULT_AUTO_FIELDgets migrations altering every implicitidtobigint. PinAutoFieldfirst, then migrate deliberately if you want big integers. -
Trusting classifiers in either direction. A package without a 6.1 classifier may work (Channels 4.3.2 did); one with it may still emit deprecation warnings. The test suite decides.
-
Forgetting migrations after automatic fixes.
null_boolean_fieldandindex_togetherchange model state;makemigrations --checkin CI catches the forgotten migration. -
Accepting every rewrite blindly. django-upgrade turns
strURL arguments intointwhen a regex becomes<int:pk>and leaves intent-dependent code (is_ajax()) alone; review the diff and run the tests. -
Matching warning classes by name.
RemovedInDjango70WarningbecomesRemovedInDjango2028Warningduring the 6.1 series; filter onDeprecationWarning/PendingDeprecationWarninginstead. -
Keeping the old cache. Pickled objects are not guaranteed to survive a version change; clear the cache after the deploy.
-
Copying a book’s settings file. Several of its settings no longer exist (
MIDDLEWARE_CLASSES,USE_L10N, the storage settings), others are insecure as printed (Errata and practices not to copy).
What changed recently
| Version (date) | Change |
|---|---|
6.1.2 (scheduled 2026-10-06) |
Security release for 6.1, with 6.0.9 and 5.2.18 alongside; per its release notes, published in advance, it fixes one low and three moderate security issues plus a data-loss bug and an infinite loop. Install it on release day. |
6.1 branch (2026-09-03) |
|
6.1.1 (2026-09-02) |
Current patch release on PyPI. |
6.1 (2026-08-05) |
Calendar versioning announced (Django 2028, 2029); PostgreSQL 15, MySQL 8.4, MariaDB 10.11 and SQLite 3.37 minimums;
|
6.0 (2025-12-03) |
Python 3.12 minimum; |
4.2 LTS (April 2026) |
End of extended support: projects still on 4.2 receive no security fixes and should move to 5.2 LTS. |
django-upgrade 1.33.0 (2026-10-02) |
Current release; supports |
The 6.1 rows come from the 6.1 release notes and the 6.1 stable branch; the support dates from the download page; the django-upgrade row from its PyPI page and changelog.
References
-
https://developers.google.com//api-shutdown[Google API shutdown]