skip to content

Deprecations and Removals

DeprecationWarning is silent by default outside __main__, so removals land as breakage. Covers surfacing them, failing CI on them, and staging a migration off an API that is going away.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

3

Why does Python hide DeprecationWarning by default, and how do you surface it?

level: juniorimportance: must knowfreq 55%

answer

  1. Who is the message actually written for?
  2. The filters decide, not the class
  3. One module escapes the blanket ignore
  4. Command line, env var, dev mode, code
  5. warnings.simplefilter and -X dev

basics

~20 s

Python's default filters ignore DeprecationWarning unless it is attributed to the main module, so end users are not flooded with warnings about libraries they did not write. Surface them with -W default::DeprecationWarning, PYTHONWARNINGS, or -X dev.

solid answer

~40 s

A deprecation is not raised, it is passed to the `warnings` machinery, which walks `warnings.filters` and acts on the first match. CPython ships with `default::DeprecationWarning:__main__` ahead of a blanket `ignore::DeprecationWarning`, so a deprecation blamed on your script or the REPL prints, while the same one raised two packages deep is dropped. The rationale is audience: `DeprecationWarning` is aimed at whoever wrote the calling code, and `FutureWarning` — which is shown by default — is aimed at end users. PEP 565 restored the `__main__` exception in Python 3.7 after a decade of total silence. To see them, pass `-W default::DeprecationWarning`, set `PYTHONWARNINGS` to the same spec, run under `-X dev`, or call `warnings.simplefilter("default", DeprecationWarning)` in code.

code

python · 14 lines
python
import warnings


def legacy():
    warnings.warn("legacy() is deprecated", DeprecationWarning, stacklevel=2)


legacy()  # blamed on __main__, so the default filter prints it

warnings.simplefilter("error", DeprecationWarning)
try:
    legacy()
except DeprecationWarning as exc:
    print("promoted to an exception:", exc)

go deeper

for a junior

Be ready to say that Python filters deprecations out unless they come from the script you ran, and to name one way to switch them on, such as running with -X dev.

for a middle

Explain the filter list itself: the order of the default entries, what the actions error, default, always and ignore do, and why -W, PYTHONWARNINGS and warnings.simplefilter are three routes to the same list.

for a senior

Show what this costs in practice: a service can use deprecated APIs for two releases with zero signal, so you deliberately surface warnings in test runs and route them to logs in production rather than discovering them at upgrade time.

for a principal

Own the policy across repositories: which categories are fatal where, how deprecation signal is collected across a fleet, and how visibility settings differ between local development, CI and production so nobody is surprised by an interpreter bump.

### The filter list is the mechanism, not the warning class `DeprecationWarning` is an ordinary exception class, but a deprecation is normally not raised. `warnings.warn(message, DeprecationWarning)` hands the category to the warnings machinery, which consults `warnings.filters` — an ordered list of tuples of the form `(action, message_regex, category, module_regex, lineno)`. Python walks that list top to bottom and the **first** entry that matches decides the action: `error` raises the warning as an exception, `default` prints it once per unique source location, `always` prints every occurrence, `module` prints once per module, `once` prints once per process, and `ignore` drops it silently. ### What a stock CPython 3.14 ships with The startup filter list is, in order, effectively: ``` default::DeprecationWarning:__main__ ignore::DeprecationWarning ignore::PendingDeprecationWarning ignore::ImportWarning ignore::ResourceWarning ``` Read top-down: a `DeprecationWarning` attributed to the module named `__main__` prints once per location; every other `DeprecationWarning` matches the second entry and is dropped before it reaches stderr. `UserWarning`, `FutureWarning`, `SyntaxWarning` and `RuntimeWarning` match no ignore entry, fall off the end of the list, and are shown. ### Why `__main__` is special The split is about audience. A `DeprecationWarning` is addressed to *the author of the calling code*, not to whoever runs the program: it says "this call site needs changing before the next release". A `FutureWarning` is the mirror image — it is aimed at end users, which is why it is visible by default. Deprecations were made silent everywhere in the 2.7/3.2 era because end users were being flooded with warnings about libraries they did not write and could not fix. That over-corrected: the people who most needed to see them — someone iterating on a script or working in the REPL — stopped seeing them too. PEP 565, in Python 3.7, restored visibility for `__main__` only, which is the compromise still in force on 3.14. ### "Attributed to" is not "raised in" The module a filter matches against is the `__name__` of the frame the warning is *blamed on*, which `warnings.warn`'s `stacklevel` argument selects. A well-written deprecation inside a library uses `stacklevel=2` so that the warning points at the caller — your file, your line number — rather than at the library's own internals. That is what makes the `__main__` rule useful: a deprecated helper called directly from a script is attributed to `__main__` and therefore printed, while the same helper called two packages deep is attributed to that package and stays quiet. It is also the reason module-scoped filters behave surprisingly: filtering "that library's deprecations" often does not work, because with a correct `stacklevel` the warning is attributed to *your* module. ### The four levers that turn them on * **Command line.** `python -W default::DeprecationWarning app.py`. The spec is `action:message:category:module:lineno`, and empty fields mean "match anything". * **Environment.** `PYTHONWARNINGS=default::DeprecationWarning` takes the same comma-separated specs, and — unlike `-W` — is inherited by child processes. * **Development mode.** `python -X dev` (or `PYTHONDEVMODE=1`) installs `default` filters and turns on a batch of other runtime checks; it is the one-flag answer for local work. * **In code.** `warnings.simplefilter("default", DeprecationWarning)` inserts a match-everything-of-this-category filter at the head of the list; `warnings.filterwarnings(...)` does the same with message and module regexes; `warnings.resetwarnings()` clears the list entirely. Test runners matter here too: `unittest` installs `default` filters for the code under test unless you passed `-W` yourself, so warnings that are invisible in production often do appear in a test run. ### Once per location, and why warnings seem to vanish Even with the `default` action, a given warning prints once per unique `(message, category, module, line number)` — a deprecated call inside a loop yields one line, not a million. Python remembers what it has already shown in a per-module registry, which is why a warning that fired in one part of a long-running process is silent later. Mutating the filter list invalidates those registries, so calling `simplefilter` mid-run can make previously-suppressed warnings reappear. Use the `always` action when you actually want every occurrence, for example when counting call sites before a migration. ### The consequence to state in an interview Because the default is silence, a deprecation that CPython or a dependency has been announcing for two releases produces *no signal at all* in a normal run, and the first thing the team sees is an `AttributeError` or a `ModuleNotFoundError` on the upgrade. Surfacing them is therefore not a nicety: it is how a scheduled removal turns into a planned migration instead of an incident.

  • What does the stacklevel argument of warnings.warn change about which filter matches?
    `stacklevel` selects the frame the warning is blamed on, and the filter's module field is matched against that frame's module. With the default `stacklevel=1` the warning is attributed to the library that raised it; with `stacklevel=2` it is attributed to the caller, so the reported file and line point at the code that needs fixing — and a deprecation called directly from a script becomes attributed to `__main__`, which is exactly what makes the default filter show it.
  • How does DeprecationWarning differ from FutureWarning and PendingDeprecationWarning?
    They differ by audience and by default visibility. `DeprecationWarning` targets the developer of the calling code and is ignored outside `__main__`. `FutureWarning` targets end users of an application and is shown by default, so use it when the people running the program need to act. `PendingDeprecationWarning` is a legacy category for a removal that is further out; it is ignored unconditionally, and most projects now just use `DeprecationWarning` earlier.
  • Besides warning filters, what else does running under -X dev turn on?
    Development mode installs `default` filters — so deprecations and `ResourceWarning` become visible — and additionally enables `faulthandler` so a crash prints a Python traceback, turns on debug hooks in the memory allocator, enables asyncio debug mode, and adds extra argument checking in places that are normally optimised for speed. It is slower and is meant for development and test runs, not production.

It is a builder's notice taped inside the wall cavity rather than on the front door: useful to whoever opens up the wall next, deliberately invisible to the people just living in the house.

saying these in an interview costs you the question

  • Claiming DeprecationWarning is never shown anywhere
  • Thinking the warning class itself decides visibility
  • Confusing it with FutureWarning, which is shown by default
  • Believing warnings.warn raises an exception by default
  • Saying you must edit site-packages to see library deprecations
  • Assuming a silent run means no deprecated APIs are used

context

open as a page

How do you make a CI run fail on DeprecationWarning without breaking on third-party ones?

level: middleimportance: should knowfreq 45%

basics

~20 s

Promote the category to an error, but scope it. Run the suite with -W ignore::DeprecationWarning -W error::DeprecationWarning:yourpackage, or set PYTHONWARNINGS to the same spec: the later filter is checked first, so only your own code is fatal.

open as a page

How do you stage a migration off stdlib modules PEP 594 removed before an upgrade?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Do the diagnosis on the interpreter you are leaving: PEP 387 guarantees two releases of DeprecationWarning, but it is filtered out by default. Surface it, diff your imports against the target's sys.stdlib_module_names, then replace modules one at a time before bumping the interpreter.

open as a page