skip to content

What warnings policy would you set with -W and PYTHONWARNINGS across a large codebase?

level: principalimportance: should knowfreq 25%

answer

  1. The audience decides the policy
  2. Different environments deserve different actions
  3. Global list, so the application owns it
  4. Escalate narrowly, observe broadly
  5. Last spec wins; module fields are exact

basics

~20 s

Escalate where someone can act and observe where they cannot: -W error scoped to your own modules in CI, warnings recorded and aggregated in staging, plain defaults in production. Libraries emit warnings; only applications configure filters.

solid answer

~50 s

I split it by environment, because a warning is only useful where somebody can act on it. CI and the test suite run with `PYTHONWARNINGS="error::DeprecationWarning"` **scoped to our own modules**, so a deprecation we introduced fails the build; a staging run opens everything up with an `always` filter and aggregates occurrences so we can see the real blast radius before promising a migration; production keeps the shipped defaults and never runs `error`, since a warning raised at import time in a dependency would take the process down for a message nobody is reading. Precedence matters when writing the specs: each `-W` or comma-separated `PYTHONWARNINGS` entry is inserted at the front of the filter list, so the **last** one wins, which is how you write a broad `error` followed by narrow exemptions. And the rule I enforce hardest: library code never calls `simplefilter` or `filterwarnings` at import — the filter list belongs to the application.

code

console · 1 line
console
python -W error::DeprecationWarning -W ignore::DeprecationWarning:__main__ -c "import warnings; warnings.warn('quiet', DeprecationWarning); print('survived')"

go deeper

for a junior

Know that -W on the command line and PYTHONWARNINGS in the environment change what a run shows, and that you can turn a warning into an error for a single run when you want to find where it comes from.

for a middle

Explain the spec format, that later specs take precedence because each is inserted at the front of the filter list, and why a test run and a production run should not use the same policy.

for a senior

Show the environment split and the scoping mechanics — exact module matching for -W specs, regex matching for filterwarnings, and the stacklevel effect that decides which module a dependency's warning is attributed to.

for a principal

Own the tradeoff: escalation buys enforcement at the cost of builds broken by other people's release schedules, so choose which categories are fatal where, fund the migration the ratchet implies, and treat warning blindness as the real failure.

### The one structural rule The filter list is process-global. That makes it an **application-level** decision, in exactly the way logging configuration is: a library emits warnings, an application decides what happens to them. A library that calls `warnings.simplefilter('ignore')` at import time has silently rewritten the policy for every other library in the process, including deprecations the application was trying to hunt down. The only filter manipulation acceptable inside library code is a narrowly-scoped `warnings.catch_warnings` block around a call whose warning you deliberately handle — and even that mutates global state for the duration, so it belongs in single-threaded code paths. ### Split the policy by environment Escalate where a failure is cheap and someone is watching; observe everywhere else. - **Developer machines**: `-X dev`. It turns on the default filters, so deprecations and unclosed-resource warnings show up while the code is being written, alongside the other development-mode checks. - **CI and the test suite**: `PYTHONWARNINGS="error::DeprecationWarning"`, narrowed to modules you own. This is the ratchet: a deprecation introduced by your own change cannot merge. Set it in the job's environment rather than in code, so it also covers warnings raised during import. - **Staging**: an `always` filter plus `logging.captureWarnings(True)`, so every occurrence is routed into the operational log and can be counted. This is where you learn how many call sites a migration actually touches, rather than the deduplicated one the default action shows you. - **Production**: the shipped defaults. Never `error`. A dependency upgrade that starts warning at import time would turn a cosmetic message into an outage, and there is nobody at the console to read the output anyway. ### Writing the specs correctly A spec is `action:message:category:module:lineno`, all fields optional. Two mechanics decide whether your policy does what you think: **Precedence.** Every `-W` option and every comma-separated entry in `PYTHONWARNINGS` is inserted at the *front* of `warnings.filters`, and matching stops at the first hit. So the specs are effectively applied last-wins: write the blanket `error::DeprecationWarning` first and the narrow `ignore::DeprecationWarning:vendor.client` after it, and the exemption takes effect. **Scoping.** The module field of a `-W` or `PYTHONWARNINGS` spec is treated as a literal module name and, since Python 3.13, must match the reported module in full — `vendor.client` does not cover `vendor.client.http`. `warnings.filterwarnings(module=...)` is the granular alternative: it takes a real regular expression matched from the start of the module name, which is what you want for `r'myapp\.'`. **The stacklevel trap.** Filters match the module a warning is *reported* from, and that is chosen by the emitting code's `stacklevel`. A dependency that correctly warns with `stacklevel=2` attributes its deprecations to *your* module, so an ignore rule scoped to the dependency never matches, and a broad `error` scoped to your own modules catches them. Scoping by module is a good default, not a guarantee; verify it with a recording run rather than assuming. **Compile-time categories.** `SyntaxWarning` is issued when a module is compiled, which for an imported module may be long before your code runs. A filter set in `main()` cannot affect it; only `-W`, `PYTHONWARNINGS` or a cached-bytecode-invalidating recompile can. Python 3.14 added one worth knowing about, from PEP 765: a `SyntaxWarning` for `return`, `break` or `continue` inside a `finally` block, because that silently discards an in-flight exception. ### Deprecations you own For your own removals, three obligations travel together: emit `DeprecationWarning` (not `UserWarning`, which is aimed at end users) with a `stacklevel` that names the caller; say in the message what to use instead; and give consumers a window measured in releases. Since Python 3.13 the `warnings.deprecated` decorator marks a function or class both at runtime and for static type checkers, so callers can be found before anything is executed — a much better ratchet than a runtime warning that only fires on covered code paths. ### The judgment The failure mode this policy is designed against is warning blindness: a build that prints two hundred deprecations nobody reads is identical, operationally, to no warnings at all. So the target is not "zero warnings everywhere" — it is a small number of categories that are *fatal* in the places you can fix them, a count you actually track in staging, and silence in production where the audience cannot act. Measure before you escalate, escalate one category or one module at a time, and never let the ratchet depend on a dependency's release schedule.

  • Why set the CI policy in the environment rather than calling filterwarnings in the test setup?
    Because filters only affect warnings issued after they are installed, and a great many warnings — anything raised during import, and every `SyntaxWarning`, which is issued at compile time — happen before any test code runs. `PYTHONWARNINGS` and `-W` are read during interpreter startup, so they cover the whole process. In-code filters remain useful for narrowing, not for the baseline policy.
  • A dependency floods your build with deprecations you cannot fix. What do you do?
    Add a narrow exemption rather than dropping the escalation: an `ignore` spec for that category scoped to the dependency's module, placed after the broad `error` so it takes precedence. Track it as debt with an owner and a review date, and verify it actually matches — if the dependency warns with `stacklevel=2` the warning is attributed to your module, and the exemption has to be written against the call site instead.
  • How do you know a deprecation campaign is finished?
    Not by the absence of log lines, since the default action deduplicates per location and hides the rest. Run staging with an `always` filter, aggregate occurrences by file and line, and drive that count to zero; then flip the category to `error` for the affected modules so it cannot come back. The escalation is the closing step, not the measurement.

saying these in an interview costs you the question

  • Sets -W error globally in production
  • Has a library call simplefilter at import time
  • Thinks the first -W spec wins over later ones
  • Escalates every category at once, then ignores the noise
  • Assumes a module-scoped filter always matches the dependency
  • Configures filters in code only, missing import-time warnings

context