skip to content

Release Targets and Upgrades

The interpreter releases a project supports and what that decision forces: which floor to declare, what to stop using, and when an upgrade pays for itself. Every senior candidate gets asked it.

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

questions

7

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

What does requires-python in pyproject.toml do when an old interpreter installs your package?

level: middleimportance: must knowfreq 55%

basics

~20 s

It declares the interpreter range the distribution supports, and lands in the built metadata as Requires-Python. An installer filters out releases whose range excludes the running interpreter and resolves to the newest release that still allows it.

open as a page

How do you use sys.version_info to guard code that only runs on newer Python?

level: juniorimportance: should knowfreq 45%

basics

~10 s

Compare sys.version_info against a plain tuple, as in sys.version_info >= (3, 12), and put the newer-only import or branch inside that guard. Never parse the sys.version string; tuple comparison is exact, cheap and unambiguous.

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

When would you put a PEP 508 marker such as python_version < '3.11' on a dependency?

level: seniorimportance: should knowfreq 45%

basics

~20 s

When a requirement applies only to some environments — a backport needed below a given interpreter, or a helper needed only on one platform. The marker is evaluated by the installer in the target environment, so one artefact serves every environment.

open as a page

When is raising a library's requires-python floor justified, and what evidence decides it?

level: principalimportance: should knowfreq 35%

basics

~20 s

Raise it when the oldest supported interpreter costs more than it earns: upstream end-of-life passed, install share has collapsed, dependencies have already moved, and shims plus test-matrix cost are real. Evidence beats taste, and old users keep getting old releases.

open as a page