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.

Supported versions and the release policy

See what a release brought

One subsection per feature release from 3.0 to 6.1: date, Python, headline features, breaking changes, removals.

Release by release: 3.0 to 6.1

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.

Upgrading Django

Rewrite old code automatically

django-upgrade --target-version 6.1 on every tracked .py file, then review the diff and finish by hand.

django-upgrade: rewriting 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.

Where the books are outdated: the consolidated table

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.

Table 1. Supported Django series on 2026-10-03 (download page)
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:

Table 2. Roadmap on the download page
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.

Table 3. Feature releases since the books
Release Date Python One-line summary

3.0

2019-12-02

3.6-3.9

ASGI, TextChoices/IntegerChoices, MariaDB; Python 2 compatibility helpers removed.

3.1

2020-08-04

3.6-3.9

Async views and middleware, models.JSONField on every backend, pathlib in the project template.

3.2 LTS

2021-04-06

3.6-3.10

DEFAULT_AUTO_FIELD, functional indexes, @admin.display/@admin.action, automatic AppConfig discovery.

4.0

2021-12-07

3.8-3.10

zoneinfo by default, functional unique constraints, scrypt hasher, Redis cache backend, template-based form rendering; url(), ugettext(), is_ajax() removed.

4.1

2022-08-03

3.8-3.11

Async ORM interface and async CBV handlers, constraint validation, as_div(), bulk_create() upserts, SECRET_KEY_FALLBACKS.

4.2 LTS

2023-04-03

3.8-3.12

psycopg 3, STORAGES, db_comment, async iterators in StreamingHttpResponse, more async model methods.

5.0

2023-12-04

3.10-3.12

Admin facets, field group templates, db_default, GeneratedField; USE_TZ=True by default; USE_L10N, pytz and GET logout removed.

5.1

2024-08-07

3.10-3.13

{% querystring %}, PostgreSQL connection pools, LoginRequiredMiddleware; index_together and the old storage settings removed.

5.2 LTS

2025-04-02

3.10-3.14

Composite primary keys, automatic model imports in shell, async auth helpers.

6.0

2025-12-03

3.12-3.14

Built-in CSP, template partials, the Tasks framework, the modern email API; BigAutoField is the default.

6.1

2026-08-05

3.12-3.14

Fetch modes, database-level on_delete, MAILERS, calendar versioning announced.

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).

timeline title Django feature releases since the books 2019 : 3.0 ASGI 2020 : 3.1 async views 2021 : 3.2 LTS : 4.0 zoneinfo 2022 : 4.1 async ORM 2023 : 4.2 LTS psycopg 3 : 5.0 db_default 2024 : 5.1 connection pools 2025 : 5.2 LTS composite keys : 6.0 CSP and tasks 2026 : 6.1 fetch modes 2027 : 6.2 LTS planned 2028 : Django 2028 planned

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 SynchronousOnlyOperation when called from an event loop); enumeration types TextChoices and IntegerChoices for choices; PostgreSQL ExclusionConstraint; boolean expressions directly in filter().

  • Changed defaults: X_FRAME_OPTIONS = "DENY" and SECURE_CONTENT_TYPE_NOSNIFF = True; FILE_UPLOAD_PERMISSIONS defaults to 0o644.

  • Removed: the private Python 2 compatibility layer (django.utils.six, python_2_unicode_compatible() and friends), render_to_response(), the {% load staticfiles %} and admin_static template tag libraries.

  • Deprecated: ugettext*() and ungettext*(), force_text() and smart_text(), urlquote() and friends (all removed in 4.0).

  • Pages: Async Support, Models & Fields, Security.

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.JSONField on every supported backend; the startproject template builds paths with pathlib (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-only JSONField, PASSWORD_RESET_TIMEOUT_DAYS, {% ifequal %}/{% ifnotequal %} (all removed in 4.0).

  • Pages: Async Support, Models & Fields, Settings & Configuration.

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_FIELD setting and AppConfig.default_auto_field (new projects and apps are generated with BigAutoField, but the global default stays AutoField until 6.0, which is why older projects show the models.W042 warning); automatic AppConfig discovery, so default_app_config is no longer needed; functional indexes (Index(Lower("title"))); the PyMemcacheCache backend; the @admin.display and @admin.action decorators; QuerySet.alias(); TestCase.captureOnCommitCallbacks().

  • Deprecated: MemcachedCache (removed in 4.1) and default_app_config (removed in 4.1).

  • Pages: Models & Fields, Constraints, Indexes & Composite Keys, Admin Customization, Caching.

Django 4.0 (2021-12-07)

  • Python: 3.8-3.10. Databases: PostgreSQL 10+, psycopg2 2.8.4+.

  • Headline features: zoneinfo is 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 the Origin header; the Cross-Origin-Opener-Policy header in SecurityMiddleware.

  • Backwards-incompatible: values in CSRF_TRUSTED_ORIGINS must include the scheme (https://bookshelf.example); DeleteView now uses FormMixin, so custom deletion logic moves from delete() to form_valid(); USE_L10N defaults to True.

  • Removed: django.conf.urls.url(), ugettext()/ugettext_lazy() and the other u aliases, force_text()/smart_text(), HttpRequest.is_ajax(), NullBooleanField (except in historical migrations), PASSWORD_RESET_TIMEOUT_DAYS, the django-admin.py entry point.

  • Deprecated: USE_L10N, pytz support (USE_DEPRECATED_PYTZ), and an announcement that USE_TZ will default to True.

  • Pages: Localization & Time Zones, Caching, Form Rendering & Widgets, URLs & Routing, Passwords.

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 for over a queryset); Model.full_clean() validates Meta.constraints; the accessible <div>-based form template and Form.as_div(); bulk_create() can update rows on conflict; Q objects support XOR; persistent connections get CONN_HEALTH_CHECKS; SECRET_KEY_FALLBACKS allows 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.

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); the STORAGES setting and InMemoryStorage; StreamingHttpResponse accepts async iterators under ASGI; more async model and related-manager methods (asave(), adelete(), aadd(), …​); BREACH mitigation in GZipMiddleware; the admin light/dark theme toggle; headers= on the test client.

  • Deprecated: Meta.index_together, DEFAULT_FILE_STORAGE and STATICFILES_STORAGE, the length_is filter, 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.

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; choices accepts mappings, callables and enumeration types directly (no .choices suffix); aauthenticate()/alogin()/alogout(); Signal.asend(); async-compatible view decorators; update_or_create(create_defaults=…​).

  • Backwards-incompatible: USE_TZ defaults to True; the default form and formset rendering style is the <div> one.

  • Removed: USE_L10N (localization is always on), pytz support and is_dst, logout via GET, PickleSerializer, GeoModelAdmin/OSMGeoAdmin.

  • Deprecated: DjangoDivFormRenderer/Jinja2DivFormRenderer, cx_Oracle (use oracledb), the http default scheme of forms.URLField (all removed in 6.0).

  • Pages: Admin Customization, Models & Fields, Authentication, Signals, Localization & Time Zones.

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); LoginRequiredMiddleware with @login_not_required; the async session API; __ lookups in list_display; AdminUserCreationForm; SQLite init_command and transaction_mode options.

  • Deprecated: CheckConstraint(check=…​) in favour of condition=, positional arguments to Model.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.

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 shell imports every model automatically; overriding BoundField per form or field (bound_field_class); ColorInput, SearchInput and TelInput widgets; async user-manager and permission helpers (acreate_user(), ahas_perm(), …​); reverse() accepts query and fragment; MySQL connections default to utf8mb4.

  • Deprecated: the all argument of the staticfiles find(), the ordering argument of the PostgreSQL aggregates, the login() fallback to request.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.

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, the csp() context processor); template partials ({% partialdef %}, {% partial %}, "template.html#name"); the background Tasks framework django.tasks (@task, enqueue(), the TASKS setting; no worker included); email built on Python’s modern email.message.EmailMessage API; AsyncPaginator; forloop.length; StringAgg and AnyValue on every backend; shell also imports common utilities.

  • Backwards-incompatible: DEFAULT_AUTO_FIELD now defaults to BigAutoField, so projects that never set it get migrations for every implicit primary key unless they pin AutoField; custom expressions' as_sql() must return parameters as a tuple.

  • Removed: cx_Oracle support, the transitional div form renderers, CheckConstraint.check, positional arguments to Model.save(), django.utils.itercompat, the FORMS_URLFIELD_ASSUME_HTTPS setting.

  • Deprecated (removed in Django 2028): positional optional arguments of the mail functions, ADMINS/MANAGERS as tuples, BadHeaderError, SafeMIMEText/SafeMIMEMultipart, the PostgreSQL StringAgg, URLIZE_ASSUME_HTTPS.

  • Pages: Content Security Policy, Template Partials & htmx, Tasks Framework, Email, Pagination.

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-level on_delete (DB_CASCADE, DB_SET_NULL, DB_SET_DEFAULT); the MAILERS setting for several named email backends; the UUID4 and UUID7 database 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' ordering argument, the login() fallback to request.user.

  • Deprecated (removed in Django 2028): the EMAIL_* settings and get_connection() in favour of MAILERS; select_related() with no arguments; list_select_related = True; values_list(flat=True) without a field; BLANK_CHOICE_DASH; transaction.savepoint() (renamed savepoint_create()); the PostgreSQL bit aggregates.

  • Pages: Query Optimization & Fetch Modes, Relationships, Email, Passwords.

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.

catalog/models.py
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:

catalog/tests.py
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);

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.

Table 4. Deprecated and removed APIs, oldest first
Old form Current form Deprecated Removed django-upgrade fixer Covered on

MIDDLEWARE_CLASSES and old-style middleware

MIDDLEWARE with new-style (callable) middleware

1.10

2.0

-

Middleware

include(…​, namespace=…​) without an application namespace

app_name = "…​" in the included URLconf, or include((patterns, app_name), namespace=…​)

1.9

2.0

-

URLs & Routing

django.utils.six, python_2_unicode_compatible()

Plain Python 3 (str, super(), __str__)

private API, no warning

3.0

-

Models & Fields

render_to_response()

render(request, template, context)

2.0

3.0

render_to_response

Function-Based Views & Shortcuts

{% load staticfiles %}, {% load admin_static %}

{% load static %}

2.1

3.0

-

Static Files & Front-End Assets

django.conf.urls.url()

django.urls.path() with converters, or re_path()

3.1

4.0

django_urls

URLs & Routing

ugettext(), ugettext_lazy(), ungettext() …​

gettext(), gettext_lazy(), ngettext() …​

3.0

4.0

utils_translation

Internationalization & Translation

force_text(), smart_text()

force_str(), smart_str()

3.0

4.0

utils_encoding

Internationalization & Translation

HttpRequest.is_ajax()

request.headers.get("x-requested-with") == "XMLHttpRequest", or request.accepts(…​)

3.1

4.0

-

Request/Response Cycle

NullBooleanField

BooleanField(null=True)

3.1

4.0

null_boolean_field

Models & Fields

django.contrib.postgres.fields.JSONField

django.db.models.JSONField

3.1

4.0

compatibility_imports

Models & Fields

PASSWORD_RESET_TIMEOUT_DAYS

PASSWORD_RESET_TIMEOUT (seconds)

3.1

4.0

password_reset_timeout_days

Authentication

MemcachedCache (python-memcached)

PyMemcacheCache, PyLibMCCache or RedisCache

3.2

4.1

-

Caching

default_app_config

Nothing: AppConfig is discovered automatically

3.2

4.1

default_app_config

Project Structure & Apps

USE_L10N

Nothing: localized formatting is always on

4.0

5.0

use_l10n

Localization & Time Zones

pytz time zones, is_dst

zoneinfo (standard library)

4.0

5.0

-

Localization & Time Zones

Logout with a GET request to LogoutView

A POST form with {% csrf_token %}

4.1

5.0

-

Authentication

django.utils.timezone.utc

datetime.timezone.utc

4.1

5.0

utils_timezone

Localization & Time Zones

Meta.index_together

Meta.indexes = [models.Index(fields=[…​])]

4.2

5.1

index_together

Constraints, Indexes & Composite Keys

DEFAULT_FILE_STORAGE, STATICFILES_STORAGE

STORAGES = {"default": …​, "staticfiles": …​}

4.2

5.1

settings_storages

File Uploads & Storage

assertQuerysetEqual()

assertQuerySetEqual()

4.2

5.1

assert_set_methods

Testing

CheckConstraint(check=…​)

CheckConstraint(condition=…​)

5.1

6.0

check_constraint_condition

Constraints, Indexes & Composite Keys

Positional arguments to Model.save()

Keyword arguments (force_insert=…​, update_fields=…​)

5.1

6.0

-

Models & Fields

cx_Oracle

oracledb

5.0

6.0

-

PostgreSQL & Database Backends

Implicit AutoField primary keys (no DEFAULT_AUTO_FIELD)

BigAutoField by default; set DEFAULT_AUTO_FIELD = "django.db.models.AutoField" to keep the old keys

warned since 3.2 (models.W042)

default changed in 6.0

default_auto_field

Models & Fields

EMAIL_BACKEND, EMAIL_HOST and the other EMAIL_* settings, get_connection()

MAILERS = {"default": {"BACKEND": …​, "OPTIONS": {…​}}}, mailers.default

6.1

Django 2028

mail_get_connection (partial)

Email

select_related() with no arguments

select_related("author", …​) or a fetch mode

6.1

Django 2028

-

Query Optimization & Fetch Modes

transaction.savepoint()

transaction.savepoint_create()

6.1

Django 2028

transaction_savepoint

Transactions, Raw SQL & Multiple Databases

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.

Table 5. How a deprecation travels through the releases
Deprecated in Warning class while it still works Removed in

5.0 or 5.1

RemovedInDjango60Warning

6.0

5.2 LTS

RemovedInDjango61Warning

6.1

6.0 or 6.1

RemovedInDjango70Warning, renamed RemovedInDjango2028Warning (Calendar versioning from Django 2028)

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_TIMEOUT and EMAIL_FILE_PATH settings; get_connection(); the connection, fail_silently, auth_user and auth_password arguments of the mail functions; positional optional arguments; BadHeaderError, SafeMIMEText/SafeMIMEMultipart and legacy MIMEBase attachments.

  • Settings: ADMINS/MANAGERS as (name, address) tuples; the transitional URLIZE_ASSUME_HTTPS, USE_BLANK_CHOICE_DASH and SIGNED_COOKIE_LEGACY_SALT_FALLBACK settings.

  • ORM: select_related() with no arguments or with True; values_list(flat=True) without a field; None in a JSONField top-level lookup will mean SQL NULL (use JSONNull() for JSON null); BLANK_CHOICE_DASH; transaction.savepoint(); percent signs in column aliases; the PostgreSQL StringAgg, OrderableAggMixin and bit aggregates.

  • Admin: list_select_related = True; get_actions() and get_action_choices() overrides without the new action_location parameter.

  • Templates and utilities: double-dot variable lookups such as {{ book..title }}; urlize defaults to HTTPS; the salted_hmac() default algorithm moves from SHA-1 to SHA-256; Paginator(orphans=…​) greater than or equal to per_page is 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):

pyproject.toml
[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.

flowchart TD A[Read the release notes of every feature release up to the target] --> B[Read the deprecation timeline for those releases] B --> C[On the current version run the tests with python -Wa and fix every warning] C --> D[Check that each dependency supports the next feature release] D --> E[Install the latest patch of the next feature release in a fresh environment] E --> F[Run django-upgrade, the full test suite with warnings on, check and makemigrations --check] F --> G{All green?} G -- no --> C G -- yes --> H[Deploy, then clear the cache] H --> I{Target reached?} I -- no --> D I -- yes --> J[Turn warnings into errors in CI]

The steps in detail

  1. 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.

  2. Resolve warnings first. On the version you run today, run the test suite with python -Wa manage.py test and fix every RemovedInDjango…​Warning that comes from your own code. A warning fixed now is a crash avoided after the upgrade.

  3. 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.

  4. 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.

  5. 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 in pyproject.toml and uv lock --upgrade-package django).

  6. Test. Run the whole suite with warnings visible, plus manage.py check (and check --deploy with production settings) and manage.py makemigrations --check --dry-run, which fails if the upgrade changed a model state, as the DEFAULT_AUTO_FIELD change in 6.0 does for projects that never set it.

  7. Deploy and clear the cache. Objects pickled into the cache by one Django version are not guaranteed to unpickle in another (cached HttpResponse objects from cache_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:

hop/models.py and hop/tests.py before the upgrade
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

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

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":

pre-commit-config.yaml (excerpt)
repos:
  - repo: https://github.com/adamchainz/django-upgrade
    rev: "1.33.0"
    hooks:
      - id: django-upgrade
        args: [--target-version, "6.1"]

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
URLs: url() becomes path() or re_path()
--- 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")),
 ]
Views: translation and encoding aliases, request.headers
--- 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})
Models: NullBooleanField and index_together
--- 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
Admin: @admin.register and @admin.display
--- 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)

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", or request.accepts("application/json") if the code really cares about the Accept header. Note that accepts() returns True when the client sends no Accept header 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 the slug route became re_path(). \w matches non-Latin letters and Django’s slug converter does not, so changing it to <slug:slug> is a behaviour change you decide on. Where a route was converted, the view now receives an int instead of a str; test those views.

  • Model changes need migrations. The null_boolean_field and index_together fixers change model state. Run makemigrations: for index_together it generates a RenameIndex operation that keeps the existing index. Keep makemigrations --check in 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.

legacy/views.py: the change made by hand
@@ -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")})

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:

legacy/tests.py
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:

settings/base.py
-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

Getting Started

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

python -m venv + pip, or uv, with dependencies declared in pyproject.toml and a lock file

Getting Started

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)

psycopg[binary] (or psycopg[c]), same ENGINE

PostgreSQL & Database Backends

os.path.join(BASE_DIR, …​) for paths in settings

V ch. 3-9; M ch. 13

pathlib in the project template since 3.1

BASE_DIR = Path(__file__).resolve().parent.parent and BASE_DIR / "templates"

Settings & Configuration

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

Deployment, Docker & Kubernetes

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

Testing

URLs and views

What the book shows Where Changed in Current form Page

from django.conf.urls import url with regex routes such as url(r'^$', …​)

TS 1.6, 8.2-8.4, ch. 16, ch. 33

deprecated 3.1, removed 4.0

path("books/<int:pk>/", …​) with converters; re_path() only for real regexes

URLs & Routing

include(…​, namespace="…​") without an application namespace

TS 8.4, 16.3.5

removed 2.0

app_name = "catalog" in the included URLconf (or a (patterns, app_name) tuple)

URLs & Routing

request.is_ajax() (an ajax_required decorator, AJAX pagination)

M ch. 5

deprecated 3.1, removed 4.0

request.headers.get("x-requested-with") == "XMLHttpRequest", request.accepts(…​), or an HX-Request check

Request/Response Cycle

The render_to_response() shortcut (not used by the four books, but common in tutorials of their era)

-

deprecated 2.0, removed 3.0

render(request, template_name, context)

Function-Based Views & Shortcuts

A plain <a href="{% url 'logout' %}"> link to log out

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 <form method="post"> with {% csrf_token %} and a submit button

Authentication

Custom deletion logic in DeleteView.delete()

Ma ch. 4; V ch. 6, 14

DeleteView uses FormMixin since 4.0

Put the logic in form_valid()

Generic Views

Function-based auth views (django.contrib.auth.views.login() and friends)

TS ch. 20

removed 2.1

LoginView, LogoutView, PasswordChangeView …​ or path("accounts/", include("django.contrib.auth.urls"))

Authentication

Helpers and settings

What the book shows Where Changed in Current form Page

ugettext(), ugettext_lazy() and the other u aliases

TS ch. 1 examples, 29.3.12, App. D; M ch. 9 (prose)

deprecated 3.0, removed 4.0

gettext(), gettext_lazy(), ngettext()

Internationalization & Translation

force_text(), smart_text()

TS 29.3.4, App. D

deprecated 3.0, removed 4.0

force_str(), smart_str()

Internationalization & Translation

django.utils.six, @python_2_unicode_compatible, Python 2 compatibility advice

TS Intro, ch. 21, 19.3, 29.3.8, App. D and F

removed 3.0

Plain Python 3: __str__, super(), f-strings

Models & Fields

MIDDLEWARE_CLASSES and old-style middleware

TS ch. 26, App. G

removed 2.0

MIDDLEWARE and new-style middleware (callable, optionally async-capable)

Middleware

{% load staticfiles %}

M ch. 4, 10; TS 13.5; Ma ch. 5

removed 3.0

{% load static %}

Static Files & Front-End Assets

USE_L10N = True in settings

M ch. 9

deprecated 4.0, removed 5.0

Delete it: localized formatting is always on

Localization & Time Zones

pytz time zones; USE_TZ as an opt-in

TS 29.3.11, App. A; all four (settings)

zoneinfo default 4.0, pytz removed 5.0; USE_TZ = True default 5.0

zoneinfo.ZoneInfo("Europe/Madrid"); aware datetimes everywhere

Localization & Time Zones

{% trans %} / {% blocktrans %}

M ch. 9

renamed 3.1 (old names still work)

{% translate %} / {% blocktranslate %}

Internationalization & Translation

Models

What the book shows Where Changed in Current form Page

Models with an implicit AutoField primary key and no DEFAULT_AUTO_FIELD

all four

DEFAULT_AUTO_FIELD 3.2 (warning models.W042); default BigAutoField 6.0

Nothing to write for new projects; pin AutoField explicitly in old ones before 6.0

Models & Fields

Meta.index_together

M ch. 5, 7, 9

deprecated 4.2, removed 5.1

Meta.indexes = [models.Index(fields=[…​])] (a RenameIndex migration keeps the existing index)

Constraints, Indexes & Composite Keys

NullBooleanField

TS 6.4.4

deprecated 3.1, removed 4.0

BooleanField(null=True)

Models & Fields

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

class Status(models.TextChoices) / IntegerChoices, choices=Status

Models & Fields

The PostgreSQL-only django.contrib.postgres.fields.JSONField

TS 6.4.9

models.JSONField on every backend 3.1; old field removed 4.0

from django.db import models; models.JSONField()

Models & Fields

ForeignKey(User, …​) or "auth.User" instead of the configured user model

M ch. 1, 6, 10; V ch. 5

not removed; recommended practice since 1.5

settings.AUTH_USER_MODEL in models, get_user_model() elsewhere, a custom user model from day one

Custom User Model & Backends

User.add_to_class() to bolt a field onto the built-in user

M ch. 6

never supported API

A custom user model, or a one-to-one profile model

Custom User Model & Backends

Full-text search vectors computed per query; pg_trgm enabled by hand in psql

M ch. 3

TrigramExtension operation already in 1.11; stored vectors with GeneratedField 5.0

A TrigramExtension() migration; a GeneratedField or SearchVectorField with a GinIndex

PostgreSQL & Database Backends

Files and caching

What the book shows Where Changed in Current form Page

STATICFILES_STORAGE (for WhiteNoise) and DEFAULT_FILE_STORAGE (for S3)

V ch. 7; Ma ch. 10

STORAGES 4.2; old settings removed 5.1

STORAGES = {"default": {"BACKEND": …​}, "staticfiles": {"BACKEND": …​}}

File Uploads & Storage, Static Files & Front-End Assets

MemcachedCache with python-memcached

M ch. 11

PyMemcacheCache 3.2; MemcachedCache removed 4.1; built-in RedisCache 4.0

django.core.cache.backends.redis.RedisCache or …​memcached.PyMemcacheCache

Caching

Admin and forms

What the book shows Where Changed in Current form Page

method.short_description = "…​" and admin_order_field attributes; admin.site.register(Model, Admin)

M ch. 8; TS ch. 19; Ma ch. 3, 7

@admin.display and @admin.action 3.2

@admin.display(description=…​, ordering=…​), @admin.action(…​), @admin.register(Model)

Admin Customization

Forms rendered by hand-written loops over fields, or {{ form.as_p }} only

Ma ch. 2; TS 12.8; V throughout

template-based rendering 4.0, as_div() 4.1, <div> default and field groups 5.0

{{ form }} with a project FORM_RENDERER, {{ form.title.as_field_group }}, reusable form templates

Form Rendering & Widgets

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 async def views under an ASGI server; Channels only for WebSockets and long-lived protocols

Async Support

SSE and the reverse proxy built as Channels HTTP consumers

Ma ch. 8

async iterators in StreamingHttpResponse 4.2

An async def view returning StreamingHttpResponse(async_generator)

Responses, Streaming, CSV & PDF

Celery (or django-background-tasks) as the only answer for background jobs

M ch. 7; TS ch. 25

django.tasks 6.0

@task + enqueue() with a worker backend; Celery when you need its features

Tasks Framework, Celery Integration

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

ContentSecurityPolicyMiddleware with SECURE_CSP / SECURE_CSP_REPORT_ONLY

Content Security Policy

Deployment settings that list ALLOWED_HOSTS only (the books never set CSRF_TRUSTED_ORIGINS)

V ch. 3, 4, 7; M ch. 13; Ma ch. 10

CSRF_TRUSTED_ORIGINS entries need a scheme since 4.0, and HTTPS forms behind a proxy need them

CSRF_TRUSTED_ORIGINS = ["https://bookshelf.example"] with a real ALLOWED_HOSTS

Security

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 PASSWORD_HASHERS; old hashes upgrade at login

Passwords

Login protection for every view by decorating or mixing in one by one

V ch. 14; TS ch. 10

LoginRequiredMiddleware 5.1

Optionally LoginRequiredMiddleware plus @login_not_required on public views

Permissions & Authorization

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

path("ws/…​", ChatConsumer.as_asgi()); test communicators also take as_asgi()

Channels & WebSockets

Two-step middleware (__init__ takes the inner app, __call__(scope) returns another callable)

Ma ch. 8

Channels 3 (ASGI 3)

A single async callable async def __call__(self, scope, receive, send), optionally based on BaseMiddleware

Channels & WebSockets

ProtocolTypeRouter without an "http" key, or with Channels' AsgiHandler for HTTP

Ma ch. 8, 10

deprecated Channels 3, removed Channels 4

ProtocolTypeRouter({"http": get_asgi_application(), "websocket": …​}), with get_asgi_application() called before importing consumers

Channels & WebSockets

An asgi.py that calls django.setup() and channels.routing.get_default_application()

Ma ch. 10

Django’s own asgi.py since 3.0

Django’s generated asgi.py extended with the ProtocolTypeRouter above (get_default_application() still exists, for Channels' own commands)

Channels & WebSockets

"channels" in INSTALLED_APPS provides the development server

Ma ch. 8

Daphne unbundled in Channels 4

pip install "channels[daphne]" and "daphne" listed before django.contrib.staticfiles (otherwise check fails with daphne.E001)

Channels & WebSockets

aioredis.create_redis() for presence and heartbeats

Ma ch. 8, 10

aioredis merged into redis-py 4.2 as redis.asyncio

redis.asyncio.Redis, or the channel layer’s groups

Channels & WebSockets

Synchronous ORM calls (for example a token lookup) inside async middleware or consumers

Ma ch. 8

SynchronousOnlyOperation since Django 3.0; async ORM 4.1

await Token.objects.aget(…​), or database_sync_to_async around sync code

Async Support

Other third-party packages and Python

What the book shows Where Changed in Current form Page

from celery import task and @task; CELERY_ALWAYS_EAGER

M ch. 7

celery.task removed in Celery 5.0; lowercase settings since Celery 4

@shared_task; CELERY_TASK_ALWAYS_EAGER with the CELERY namespace; enqueue from transaction.on_commit()

Celery Integration

DRF @detail_route / @list_route

M ch. 12

deprecated DRF 3.8, removed DRF 3.10

@action(detail=True, methods=["post"])

Django REST Framework

DRF Core API schemas (coreapi) for API documentation

TS 16.7-16.8

deprecated DRF 3.10 (OpenAPI), removed DRF 3.17.0 (2026-03-18)

OpenAPI 3 with drf-spectacular

Django REST Framework

filter_fields on DRF views

Ma ch. 6

django-filter 2

filterset_fields (or a FilterSet class)

Django REST Framework

django-crispy-forms 1.x with CRISPY_TEMPLATE_PACK = "bootstrap4"

V ch. 10

crispy-forms 2.0 (2023-02-13) moved the Bootstrap packs out

pip install crispy-bootstrap5, add it to INSTALLED_APPS, set CRISPY_ALLOWED_TEMPLATE_PACKS and CRISPY_TEMPLATE_PACK

Forms

Bootstrap 4 with jQuery and Popper from a CDN; jQuery $.ajax with js-cookie for CSRF

V ch. 10; Ma ch. 2, 4; M ch. 5, 6, 10

Bootstrap 5 dropped jQuery

Bootstrap 5 (or none), fetch() sending the X-CSRFToken header, or htmx

Template Partials & htmx

model_mommy for test data

TS ch. 22

renamed model_bakery

model_bakery or factory_boy

pytest, Factories & Browser Tests

self.assertEquals(…​) in tests

Ma ch. 4, 6, 8; TS 22.3

alias removed in Python 3.12

self.assertEqual(…​)

Testing

Per-test setUp() creating the same rows for every test

V ch. 4-10

setUpTestData() since 1.8, isolated per test since 3.2

@classmethod setUpTestData(cls) for read-only fixtures

Testing

Pillow Image.ANTIALIAS in a thumbnail signal

Ma ch. 3

removed in Pillow 10.0

Image.Resampling.LANCZOS

Signals

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

Authentication

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; MAILERS (6.1)

Email

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

Deployment

raven for error reporting to Sentry

Ma ch. 10

replaced by sentry-sdk

sentry-sdk with its Django integration, plus Django’s own logging and ADMINS email reports

Logging & Error Reporting

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

SECRET_KEY (and email or API credentials) committed in settings.py

all four

Read secrets from the environment or a secret manager; generate keys with get_random_secret_key()

Settings & Configuration

ALLOWED_HOSTS = ["*"] and DEBUG = True in production; check --deploy warnings ignored

V ch. 3, 4, 7; M ch. 13; Ma ch. 10

Real host names, DEBUG = False, and a clean manage.py check --deploy

Deployment, Security

State changes through GET (add to basket, logout links)

Ma ch. 4; all four (logout)

POST forms with {% csrf_token %}; require_POST on such views

Security

CsrfExemptMixin to make AJAX work

M ch. 10

Keep CSRF protection and send the token in the X-CSRFToken header

Security

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

Security

Markdown rendered and passed through mark_safe() unsanitised; innerHTML with server data

M ch. 3; Ma ch. 8

Sanitise the HTML (for example with nh3) before marking it safe; textContent in JavaScript; format_html() in Python

Custom Template Tags & Filters

A ModelViewSet that returns every customer’s orders; fields = "__all__" in a model form

Ma ch. 6; V ch. 6

Scope get_queryset() to request.user; list fields explicitly

Django REST Framework, Model Forms & Formsets

Orders and payments written without transaction.atomic(); tasks enqueued before the commit; no price snapshot on order lines

M ch. 7-9; Ma ch. 4

atomic() around the write, transaction.on_commit() for the task, copy the price into the order line

Transactions, Raw SQL & Multiple Databases

Broken listings: a REST_FRAMEWORK syntax error, a misnamed middleware, a crashing --days option

M ch. 12-13

Run every example; this section’s listings were all executed

Management Commands & Shell

Deleted coupon or basket ids left in the session

M ch. 9; Ma ch. 4

Handle DoesNotExist and drop the stale key from the session

Sessions, Cookies, Messages & Signing

Sync ORM on the event loop, a token in the WebSocket query string, Redis KEYS polling, unclosed connections, a bytes-vs-str bug

Ma ch. 8

Async ORM or database_sync_to_async; cookie or ticket authentication; channel layer groups or SCAN; close what you open

Channels & WebSockets

Tests that pass for the wrong reason (a POST asserting 200, a delete test that never deletes, args='1')

V ch. 6

Assert the redirect and the database state; pass args=[1]

Testing

An ownership check in dispatch() that runs before LoginRequiredMixin

V ch. 14

Put LoginRequiredMixin leftmost, or use UserPassesTestMixin

Permissions & Authorization

User.add_to_class(); no tests at all

M ch. 6; M throughout

A custom user model; a test for every view and form

Custom User Model & Backends, Testing

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 Support

Async views, middleware and test client

3.1

Async Support

Async cache API

4.0

Caching

Async ORM interface, async CBV handlers

4.1

Making Queries

Async iterators in StreamingHttpResponse

4.2

Responses, Streaming, CSV & PDF

aauthenticate(), alogin(), Signal.asend()

5.0

Signals

Async session API

5.1

Sessions, Cookies, Messages & Signing

Async user-manager and permission helpers

5.2

Authentication

AsyncPaginator

6.0

Pagination

QuerySet.alias(), functional indexes

3.2

Aggregation, Expressions & Functions

Functional unique constraints

4.0

Constraints, Indexes & Composite Keys

Constraint validation in full_clean(), bulk_create() upserts

4.1

Constraints, Indexes & Composite Keys

psycopg 3

4.2

PostgreSQL & Database Backends

db_default, GeneratedField

5.0

Models & Fields

PostgreSQL connection pools

5.1

PostgreSQL & Database Backends

Composite primary keys

5.2

Constraints, Indexes & Composite Keys

Fetch modes, database-level on_delete, UUID4/UUID7

6.1

Query Optimization & Fetch Modes

Template-based form rendering

4.0

Form Rendering & Widgets

as_div()

4.1

Form Rendering & Widgets

Field group templates, admin facets

5.0

Admin Customization

{% querystring %}

5.1

Templates

Template partials

6.0

Template Partials & htmx

Scrypt hasher

4.0

Passwords

SECRET_KEY_FALLBACKS

4.1

Settings & Configuration

LoginRequiredMiddleware

5.1

Permissions & Authorization

Built-in Content Security Policy

6.0

Content Security Policy

Automatic model imports in shell

5.2

Management Commands & Shell

django.tasks (no worker included)

6.0

Tasks Framework

Email on the modern EmailMessage API

6.0

Email

MAILERS

6.1

Email

Granian deployment how-to (ASGI and WSGI)

docs for 6.1

Deployment

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 ImportError or TypeError at 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 test says nothing about the next release. Use -Wa locally and warnings-as-errors in CI.

  • Letting 6.0 change your primary keys. A project that never set DEFAULT_AUTO_FIELD gets migrations altering every implicit id to bigint. Pin AutoField first, 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_field and index_together change model state; makemigrations --check in CI catches the forgotten migration.

  • Accepting every rewrite blindly. django-upgrade turns str URL arguments into int when 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. RemovedInDjango70Warning becomes RemovedInDjango2028Warning during the 6.1 series; filter on DeprecationWarning/PendingDeprecationWarning instead.

  • 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

Table 6. Upgrade-relevant changes, newest first
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)

RemovedInDjango70Warning renamed RemovedInDjango2028Warning, the old name kept as an alias; ships with the next 6.1 patch release.

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; EMAIL_* settings deprecated in favour of MAILERS; select_related() without arguments deprecated.

6.0 (2025-12-03)

Python 3.12 minimum; BigAutoField default; removal of everything deprecated in 5.0 and 5.1.

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 --target-version 6.1 with the 6.1 fixers (mail_get_connection, transaction_savepoint, …​).

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.