The upgrade sequence, and the warnings that are silent by default
coreadvancedDjango removes a feature only after warning about it for at least two feature releases: a feature deprecated in `A.x` "will continue to work in all A.x versions but raise warnings", and is removed in `B.0` (or `B.1` for a late deprecation). The warning is named after the release that removes it — `RemovedInDjango60Warning` — and it is **silent by default**, so your test suite passes while telling you nothing until you turn warnings on.
Think of it as
The reason Django upgrades go wrong is almost never that the new version broke something without notice. It is that the notice was given, in the form of a warning nobody displayed, across two releases nobody was reading the notes for. So the whole discipline is to make the warning visible early and to treat it as work rather than noise. The policy gives you the schedule. A feature deprecated in a feature release keeps working for the rest of that series while raising a `RemovedInDjangoXXWarning`, and disappears two feature releases later — which means that at any moment your code is carrying a list of things that will stop working on a date you can already read. The LTS relationship is worth internalising if you upgrade LTS to LTS, which most teams do: shims added in `X.0` and `X.1` are dropped in `Y.0`, shims added in `X.2` (the LTS) are dropped in `Y.1`, and `Y.2` — the next LTS — drops no shims at all, precisely "to ease LTS-to-LTS upgrades". The practical consequence is that jumping straight from one LTS to the next means absorbing every deprecation from the intervening series in a single change, so the alternative — stepping through each feature release — trades one large risky upgrade for three small ones, and is usually the better trade when the suite is good. The roadmap's own sequence encodes the order that keeps risk small: read the release notes first, because that is where backwards-incompatible changes are listed and there is no substitute; identify the deprecations affecting you; update dependencies before Django itself, since third-party packages are the most common blocker and a package that does not support the new version turns the upgrade into a fork or a wait; run the tests; fix warnings; test migrations, because `makemigrations` on a new Django can generate a migration you did not intend; benchmark the paths that matter; and deploy gradually rather than everywhere at once. Two habits make the "fix warnings" step tractable. Run the suite with warnings visible and, once clean, make them errors so a newly deprecated call cannot be merged. And do the work continuously: a deprecation fixed the week it appears is a small diff, while the same fix made two years later is a migration project with no tests written for it.
What we're doing: Make deprecation warnings impossible to ignore, so the upgrade work happens continuously instead of in one large jump.
- 4–9
- Errors for Django's own deprecations, scoped by module so the rule applies to code you control. This is what converts "we will deal with it at upgrade time" into a failing test on the day the deprecation lands.
- 11–14
- Third-party warnings are visible but not fatal, because you cannot fix another package's internals and a hard failure there would block unrelated work.
- 17–23
- The same policy expressed for pytest, so it applies whether the suite is run through Django's runner or directly. Keeping the two in step avoids a green local run and a red CI run.
- 26–30
- The audit command, run *before* planning an upgrade. `-Wd` is what makes the warnings appear at all — without it the same run prints nothing and looks clean.
- 36–37
- Grouping matters for estimating. One deprecated setting used in twelve tests is a single change; counting raw occurrences makes a two-hour task look like a sprint.
Why this works: A newly deprecated Django call fails in CI on the day it is introduced, third-party deprecations stay visible without blocking, and the size of the next upgrade is a number you can read at any time.
Upgrading LTS to LTS in one step with warnings never enabled
Wrong
Better
What you see: An upgrade branch that lives for months, is rebased weekly, accumulates conflicts with ordinary work, and is eventually abandoned — leaving the project on an LTS that is approaching end of security support.
Why: Django's policy is designed to spread the work: a deprecation is announced two feature releases before it bites, which is ample time if you are reading the warnings. Jumping LTS to LTS skips the announcements entirely, so every removal from the intervening series arrives at once, in a single branch, with no incremental verification — and the failures interact, making each one harder to diagnose than it would have been alone. Stepping through the feature releases means each upgrade is small, individually deployable, and verifiable in production before the next one begins. The remaining case for the big jump is a project with a weak test suite, where every intermediate release is equally unverifiable — and there the honest first step is the tests, not the upgrade.
- A horizontal timeline of four Django releases: 4.2 marked LTS, then 5.0, then 5.1, then 5.2 marked LTS.
- A wide amber band spans from 4.2 through 5.0, labelled: the feature still works, and calling it raises RemovedInDjango51Warning.
- At 5.1 a red marker shows the feature removed outright, with the note that code still calling it now raises an error.
- A caption under the band notes that the warning is silent by default and is enabled with python -Wd.
- A footer notes the LTS-to-LTS consequence: jumping from 4.2 straight to 5.2 absorbs every deprecation from 5.0 and 5.1 in a single change.
Django's own worked example of the policy
Together
The sequence, and what each step exists to catch
Together
Remember: Django removes nothing without at least two feature releases of warning: a feature deprecated in `A.x` works through that series, raises `RemovedInDjangoXXWarning` named for the release that removes it, and disappears in `B.0` or `B.1`. Those warnings are silent by default, so turn them on and make Django's own ones errors — that is what makes the work continuous rather than a project. Follow the sequence: release notes, deprecations, dependencies first, tests, warnings, migrations, benchmarks, gradual deploy. And prefer stepping through feature releases to jumping LTS to LTS, because the next LTS deliberately drops no shims — the cost of skipping is paid all at once in one branch.
See also: python versions packages and migration behaviour · the pipeline and where it runs · rolling blue green and canary · versioning backward compatibility and deprecation

