This is part of a series of posts I’m doing as a sort of Python/Django Advent calendar, offering a small tip or piece of information each day from the first Sunday of Advent through Christmas Eve. See the first post for an introduction.
Let this be a warning
Python provides the ability to issue a warning as a step below raising an exception; warnings are issued by calling the warnings.warn()
function, which at minimum should be called with a message and a warning type.
One of the most common ways warnings are used is to provide notification that an API is deprecated and will be removed in the future; this typically uses the DeprecationWarning
type, and the message should give some hint about what to use instead of the deprecated API, and when the deprecated API will be removed.
To take a real-world example, Django uses a two-release deprecation cycle: when an API is first deprecated, it begins issuing a PendingDeprecationWarning
, which then upgrades one release later to a DeprecationWarning
, after which the API can be removed.
Django also tends to use subclasses of the built-in warnings but named for the specific Django version which will see the removal. For example, Django 5.0 includes a DeprecationWarning
subclass named RemovedInDjango51Warning
, used as the final warning for APIs which will be removed in Django 5.1.
Django doesn’t use SemVer
You may be wondering why an API can be removed from Django without needing a major version bump, since that’s what Semantic Versioning (“SemVer”) would require. But Django does not use SemVer as its versioning scheme; instead, Django’s deprecation cycle is based on its long-term-support (“LTS”) releases, which are always the third and final release in a major version of Django. Django 2.2, 3.2, and 4.2 were all LTS releases, and 5.2 will be one when it comes out sometime in 2025.
The API compatibility policy, broadly speaking, is that code which runs on an LTS without emitting any deprecation warnings will also run on the next LTS without needing to be changed. So an application that runs on Django 3.2 LTS without deprecation warnings would be able to also run on Django 4.2 LTS (though it would need to handle any new deprecation warnings to become ready for 5.2).
But you have to look for them
Python’s default behavior is to ignore several categories of warnings, meaning that even when they’re being issued, you won’t see them show up in logs. Which means that you won’t usually see deprecation warnings, and thus can be caught by surprise when they suddenly upgrade into errors because you’re using an API that went away.
If you’re using the pytest
testing framework, it changes this for you and shows warnings by default.
But you can also change this by invoking Python with the -W
flag and specifying a warning policy, or by setting a warning policy in the PYTHONWARNINGS
environment variable. You can check the warning filter documentation for full details, and use it to adjust how often you want to see warnings; you can have them print always
, once
, ignore
, or other options (telling Python not to always re-display the same warning can be helpful if, say, you call a function hundreds of times and it emits a deprecation warning every time — that can quickly fill your logs with noise).
I generally use a policy of once::DeprecationWarning
for my own projects, but you should play with it and settle on something that works for you.
Also note that you can set a policy of error
to tell Python to turn any emitted warning into an exception, and quite a few command-line tools support this too. For example, the Sphinx documentation builder has its own -W
command-line flag which turns warnings into errors, which is useful for things like the spell-checking extension — it emits warnings for misspelled words, and the -W
flag will turn them into errors. I use this to break my CI builds in case of misspellings in documentation.