skip to content

Why does a DeprecationWarning raised by a library never appear when you run your app?

level: middleimportance: must knowfreq 50%

answer

  1. Not every warning reaches the terminal
  2. The category decides the default action
  3. An ordered list decides, first match wins
  4. One shipped entry names a single module
  5. -W, PYTHONWARNINGS and -X dev override it

basics

~20 s

Python ships default entries in warnings.filters that ignore DeprecationWarning everywhere except when it is reported from main, because the category targets developers rather than end users. Run with -W default::DeprecationWarning, PYTHONWARNINGS or -X dev to see it.

solid answer

~40 s

`warnings.warn` does not print unconditionally: the message is matched against `warnings.filters`, an ordered list of `(action, message, category, module, lineno)` entries, and the **first** match decides the action. CPython's shipped defaults are `ignore` for `DeprecationWarning`, `PendingDeprecationWarning`, `ImportWarning` and `ResourceWarning`, with one earlier entry that applies `default` to `DeprecationWarning` reported from `__main__`. So a deprecation raised inside an installed library is silent, while the same warning from the script you are running is shown. That split dates from PEP 565 in Python 3.7: 3.2 had hidden `DeprecationWarning` from end users entirely, and 3.7 gave it back to the author of the top-level script. To surface them you override the filters — `python -W default::DeprecationWarning`, `PYTHONWARNINGS=default::DeprecationWarning`, `-X dev`, or `warnings.simplefilter` in your own entry point; test runners typically enable them for you.

code

console · 1 line
console
python -W always::DeprecationWarning -c "import warnings; warnings.warn('legacy path', DeprecationWarning)"

go deeper

for a junior

Know that warnings.warn reports a problem and keeps running, that it writes to standard error rather than through logging, and that DeprecationWarning is usually invisible unless you ask for it.

for a middle

Be ready to describe warnings.filters as an ordered list where the first match wins, list the shipped defaults, and show two ways to override them: -W on the command line and PYTHONWARNINGS in the environment.

for a senior

Show the production split you would run: deprecations escalated to errors in CI where someone can fix them, left non-fatal in production, and scoped by module so a dependency's warnings do not break your build.

for a principal

Own the argument for why the default is silence: a warning aimed at an audience that cannot act on it is noise, and every category choice a library makes is really a decision about who is expected to change their code.

### Warnings are not exceptions and not logging `warnings.warn(message, category)` reports a condition the program can survive: a call site that will stop working, a resource that was not closed, a construct whose meaning is about to change. Unlike `raise`, it returns and execution continues. Unlike a log call, it is routed by a filter chain rather than by a logger hierarchy, and by default it is written to `sys.stderr` by `warnings.showwarning`, with no configuration required. Every warning has a **category**, which is a class deriving from `Warning` (itself a subclass of `Exception`). The category is how the machinery decides who the message is for: - `UserWarning` — the default when you pass no category; a message for whoever is running this code. - `DeprecationWarning` — for *developers*: something this code calls will be removed. - `PendingDeprecationWarning` — the same, further out. - `FutureWarning` — for *end users*: behaviour will change under them. It is shown by default. - `RuntimeWarning`, `SyntaxWarning`, `ImportWarning`, `ResourceWarning`, `BytesWarning`, `EncodingWarning` (added in 3.10) — the interpreter's own categories. ### The filter chain `warnings.filters` is an ordered list of five-tuples: an action, a compiled regex matched against the message text, a category class, a regex matched against the *module* the warning is reported from, and a line number. A warning is checked against each entry in order and the **first** entry that matches wins; if nothing matches, the implicit `default` action applies. The actions are `error` (raise the category as an exception), `always` (print every time), `default` (print once per unique location), `module` (once per module), `once` (once per process for that message and category) and `ignore`. On a plain `python app.py` in 3.14 the list is, in order: `default` for `DeprecationWarning` in `__main__`, then `ignore` for `DeprecationWarning`, `PendingDeprecationWarning`, `ImportWarning` and `ResourceWarning` anywhere. Read top to bottom, that is the whole answer to the question. Your library's deprecation is reported from a module that is not `__main__`, so it falls through to the blanket `ignore` and disappears. The same `warn` call in the script you launched matches the first entry and prints. ### Why it is built that way Python 3.2 silenced `DeprecationWarning` by default because end users of applications were being shown messages they could do nothing about — the person running a program cannot fix a library's use of a deprecated API. That, predictably, meant developers never saw them either, and deprecations stayed unfixed until the removal broke everything. PEP 565, in Python 3.7, restored visibility exactly where the person who can act is likely to be looking: code in `__main__`. Libraries stayed quiet; scripts and REPL sessions started talking again. ### Making them visible on purpose Four levers, in rough order of scope: 1. **Command line**: `python -W default::DeprecationWarning app.py`. The spec is `action:message:category:module:lineno`, all fields optional. Multiple `-W` options are allowed and the last one given takes precedence, because each is inserted at the front of the filter list. 2. **Environment**: `PYTHONWARNINGS="default::DeprecationWarning"`, comma-separated for several specs — the way to configure a container or a CI job without touching the entry point. 3. **Development mode**: `-X dev` (or `PYTHONDEVMODE=1`) turns on the default filters, so `DeprecationWarning` and `ResourceWarning` become visible everywhere, along with other runtime checks. 4. **In process**: `warnings.simplefilter('default')` or `warnings.filterwarnings('error', category=DeprecationWarning, module=r'myapp\.')` from your application's entry point — never from library import code, which has no right to rewrite a process-wide policy. The common production pattern is to make deprecations fatal where they can be fixed and invisible where they cannot: `-W error::DeprecationWarning` in CI, plain defaults in production. A test runner that enables warnings for you is doing the same thing one layer up. ### The trap Because the `module` field is matched against the module the warning is *reported* from, and the reported location is chosen by the `stacklevel` argument, a library that warns with the default `stacklevel=1` attributes the warning to itself; one that sets `stacklevel=2` attributes it to its caller — which changes which filters match. A deprecation warned with `stacklevel=2` out of a library called directly from your script is reported from `__main__`, and therefore shows up under the shipped defaults. Two libraries can behave differently for exactly this reason.

  • Which category should a library pick when the change will affect the people running the application, not just its developers?
    `FutureWarning`. It exists precisely for behaviour changes an end user should know about, and it is not on CPython's shipped ignore list, so it prints under default filters. `DeprecationWarning` is the developer-facing counterpart and is ignored outside `__main__`; `PendingDeprecationWarning` is for removals further out and is ignored everywhere by default.
  • How would you make an entire test run fail on any DeprecationWarning coming from your own package?
    Set `PYTHONWARNINGS="error::DeprecationWarning"` for the run, or add `warnings.filterwarnings('error', category=DeprecationWarning, module=r'myapp\\.')` in the suite's setup. Scoping by module matters: a blanket `error` also escalates warnings raised inside third-party code you cannot fix, which turns an unrelated dependency release into a red build.
  • What does -X dev change about warning behaviour?
    Development mode enables the default warning filters, so `DeprecationWarning`, `PendingDeprecationWarning` and `ResourceWarning` become visible everywhere rather than only in `__main__`, and it switches on extra runtime checks such as the debug allocator hooks and faulthandler. `PYTHONDEVMODE=1` is the environment-variable form. It is a development and CI setting, not a production one.

saying these in an interview costs you the question

  • Says warnings.warn raises and stops execution
  • Thinks warnings go to stdout, or need logging configured
  • Believes DeprecationWarning is printed on every call by default
  • Cannot name any way to reveal ignored warnings
  • Confuses FutureWarning and DeprecationWarning audiences
  • Suggests deleting the warn call as the fix

context