What one jump does to the packages
Link to this sectionMost of the work in an upgrade sits in the packages, and their order decides how risky it gets. An update that still runs on your current Django goes live on its own, before Django changes. One that has dropped your current Django has to land in the same change as Django.
I planned both routes for an example project: Django 3.2.18 on Python 3.9 with 14 Django packages, heading for 5.2.
| Route | Before Django | Together with Django | Check by hand | Ready |
|---|---|---|---|---|
| 3.2 to 5.2 in one jump | 2 | 9 | 3 | 0 |
| Step 1: 3.2 to 4.2 | 11 | 3 | 0 | 0 |
| Step 2: 4.2 to 5.2 | 10 | 0 | 3 | 1 |
In one jump, nine packages change in the same deploy as Django itself, django-allauth across 13 major versions among them. If something breaks afterwards, ten things changed at once. Through 4.2, three packages move with Django in the first step and none in the second. Everything else goes live one at a time.
| Package | Today | Step 1 | Step 2 |
|---|---|---|---|
| djangorestframework | 3.14.0 | 3.15.0 | 3.16.0 |
| django-filter | 22.1 | 23.1 | 25.1 |
| django-allauth | 0.52.0 | 0.55.0 | 65.7.0 |
| django-crispy-forms | 1.14.0 | 2.1, with Django | 2.4 |
Three packages need a look on either route: django-model-utils, django-storages and django-taggit do not declare 5.2 in their newest release.
Both plans come from django-upgrade-report 1.1.0, run on 10 October 2026. The project is invented but typical. Its requirements and the full plans are listed under sources.
Why the warnings only reach one LTS ahead
Link to this sectionDjango removes a feature in two stages. First it raises a deprecation warning, two feature releases later the feature is gone. On an LTS you therefore see a warning for everything the releases up to the next LTS remove. Fix the warnings on 3.2, and the 37 features removed in 4.0 and 4.1 are dealt with before the version changes.
That reach ends at the next LTS. The 43 features removed in 5.0 and 5.1 were deprecated in the 4.x releases. Django 3.2 does not know them and warns about none. A jump from 3.2 to 5.2 meets them as errors, in whatever order the tests hit them. On 4.2 the same 43 are warnings that name the line they come from.
Django's own upgrade guide goes one step finer and recommends every feature release in turn, each on its newest patch release. A workable middle: the releases in between run in CI, the LTS releases go live. The warnings are silent by default. Switch them on for the test run:
python -Wa manage.py testWhere the packages gather: the LTS releases
Link to this sectionOne example project proves little. So I publish the same check for the whole ecosystem, every week: the newest release of the 300 most downloaded Django packages on PyPI, judged against every Django version from 4.2 on.
Django ecosystem readiness, updated every Monday (opens a new tab)

| Django | Ready | To check | Blocked | Declared after 1 year |
|---|---|---|---|---|
| 4.2 LTS | 148 | 109 | 43 | 49 % |
| 5.0 | 104 | 153 | 43 | 44 % |
| 5.1 | 111 | 153 | 36 | 44 % |
| 5.2 LTS | 168 | 131 | 1 | 51 % |
| 6.0 | 116 | 180 | 4 | too new |
| 6.1 | 42 | 251 | 7 | too new |
Two things stand out. The LTS releases are where the packages are today: 168 are ready for 5.2 and 148 for 4.2, against 104 for 5.0 and 111 for 5.1. Counted by downloads the gap is wider: the packages ready for 5.2 account for 79 % of the downloads, those ready for 5.0 for 37 %. In the first year after a release the lead is small: 51 % of the packages declared 5.2 within a year and 49 % declared 4.2, against 44 % each for 5.0 and 5.1. A plan that stops on LTS releases stops where the packages are.
The second: support for old versions goes away. The newest release of 43 of the 300 packages excludes Django 4.2, only one excludes 5.2. That is why every step of a plan names the oldest release that declares the target and not the newest. The older releases stay on PyPI, so the route through 4.2 remains open. They get no fixes any more, which is one more reason to keep that stop short.
New releases start slowly. Django 6.1 came out on 5 August 2026. After 60 days, 13 % of the packages had a release that declares it.
Ready means the newest release declares the version. To check means the metadata says neither way, which is not the same as incompatible. Blocked means it excludes the version. The one-year share counts every package with a release that declared the version by then, including those whose newest release has dropped it since. The packages were selected by name, and downloads are not installations.
Python and the database move too
Link to this sectionEach LTS also sets which Python and which PostgreSQL it runs on:
| Django | Python | PostgreSQL | Security updates until |
|---|---|---|---|
| 3.2 LTS | 3.6 to 3.10 | 9.6 or newer | 1 April 2024 |
| 4.2 LTS | 3.8 to 3.12 | 12 or newer | 7 April 2026 |
| 5.2 LTS | 3.10 to 3.14 | 14 or newer | April 2028 |
Django 3.2 and 5.2 share exactly one Python version, 3.10, and Python 3.10 itself reached its end of life in October 2026. A direct jump therefore happens on Python 3.10, or it changes Python and Django in the same step. Django 4.2 runs on 3.8 to 3.12. That makes it the place to move Python while Django stands still.
In the example project, two packages need a newer release before they run on Python 3.12: Pillow and psycopg2-binary. The installed releases have no wheel for it. Both updates go live before the switch.
The path in six steps
Link to this sectionFor the example project, the whole way looks like this. Every step goes live on its own and has a way back.
- Django 3.2 to its last patch release, 3.2.25. Run the tests with warnings on and fix every deprecation warning.
- Update the 11 packages whose next release still runs on 3.2, one at a time.
- Django 4.2, in one change with the three packages that need it.
- Python 3.12 on Django 4.2, after Pillow and psycopg2-binary. Then fix the deprecation warnings that 4.2 raises.
- Update the 10 packages whose next release still runs on 4.2. Read the changelogs of the three that do not declare 5.2.
- Django 5.2, on its own.
That is more deploys than one jump. Each of them changes one thing, so a failure points at its cause. If your PostgreSQL is older than 14, its upgrade is one more step before the last one.
Plan the path for your own project
Link to this sectionBoth plans above come from django-upgrade-report, a free open-source tool I maintain. Since version 1.0 it plans the whole way instead of a single step. In the project directory:
uvx django-upgrade-report --target 5.2 --via ltsIt reads your lockfile or requirements and prints one plan per LTS on the way, each starting where the one before ends. It lists what Django removed on the way and where your code still uses it, and says what your dependencies need on the new Python. With --emit it prints the commands in the right order, for uv, Poetry, PDM, Pipenv or pip.
It needs Python 3.10 or newer, but neither Django nor your virtualenv. Your code is read locally and never sent. Only names and versions of packages from PyPI go to PyPI.
django-upgrade-report on GitHub (opens a new tab)
The plan shows what maintainers declare, not whether your tests pass. Plan with it, then let django-upgrade rewrite the code and let the tests decide.