skip to content

What Python warnings policy do you set for a fraud-scoring service's CI and production runs?

level: principalimportance: should knowfreq 26%

answer

  1. Two audiences, two configurations
  2. Fatal where someone can act
  3. Silence is a debt, not a fix
  4. A filter set in main() is late
  5. Route them into the log pipeline

basics

~20 s

Make warnings fatal where they can be acted on and observable where they cannot: PYTHONWARNINGS=error in the test job with a short reviewed list of pinned ignores, and logging.captureWarnings(True) in production so warnings reach the same log pipeline as everything else.

solid answer

~40 s

Split the policy by audience. In CI, run the regression pack with `PYTHONWARNINGS=error` so any new `DeprecationWarning` fails the build while there is still time to act, plus a short, reviewed list of `ignore` entries pinned to a specific module and message for dependencies you cannot fix yet - each with an owner and a removal date. In production, never promote warnings to errors: a deprecation must not take down scoring traffic. Instead call `logging.captureWarnings(True)` at startup so warnings flow through the standard logging pipeline and can be counted and alerted on. Set filters through `-W` or `PYTHONWARNINGS` at interpreter start, not in your entry point, because import-time warnings fire before your code runs. On the emitting side, own your own deprecations with `stacklevel=2` and a library-specific category.

code

console · 1 line
console
PYTHONWARNINGS='error,ignore:legacy cursor API:DeprecationWarning:some_driver.pool' python3 -m unittest discover

go deeper

for a junior

Know the two levers by name - PYTHONWARNINGS or -W to set policy for a run, and logging.captureWarnings(True) to send warnings into the logging pipeline instead of standard error.

for a middle

Be able to explain why a test run and a production process want different actions for the same category, and how a pinned ignore entry differs from silencing a whole category.

for a senior

Show that you configure policy before imports run, that you monitor deprecation volume in production rather than promoting warnings to errors there, and that you can justify each exemption.

for a principal

Own the policy as an upgrade strategy: what blocks a build, who owns each exemption and when it expires, how your own deprecations reach downstream teams, and how the rate is observed over a release cycle.

### The question behind the question A warning policy is a decision about **who is expected to act, and when**. Two audiences, two answers. A developer running the 340-case regression pack can fix a deprecated call today, so the strongest possible signal - a failed build - is appropriate. An operator watching a fraud-scoring service at 3 a.m. cannot fix a deprecated call, and promoting warnings to exceptions there converts a paper cut into an outage. So the same process is configured differently in the two environments, and that asymmetry is the answer. ### CI: fatal, with a debt register Run the test job with `PYTHONWARNINGS=error`. Every warning becomes an exception at the point it is emitted, which means the failing test names the exact call site. The immediate objection is real: a large dependency tree emits warnings you did not cause and cannot fix this sprint. Handle it with **pinned** exemptions, not a blanket retreat: ``` PYTHONWARNINGS=error,ignore:legacy cursor API:DeprecationWarning:some_driver.pool,ignore::ResourceWarning:third_party.client ``` Each entry names a message pattern and a module, so a *different* deprecation from the same package still fails. Keep the list in one reviewed file with a comment per line: who owns it, which upstream issue tracks it, when it is reviewed again. The list is a debt register; if it only ever grows, the policy has quietly become `ignore`. The alternative many teams reach for - promoting only their own category - is weaker but defensible as a first step: make `error` apply to your own library's warning base class and `default` to everyone else's, then tighten. ### Production: observable, never fatal In the service, call `logging.captureWarnings(True)` once during startup. From that point the warnings machinery routes messages to the standard logging pipeline instead of writing to standard error, so they land in the same aggregation as every other event and can be counted, sampled and alerted on. That is the crucial win: a deprecation rate that appears on a dashboard is a signal you can act on before the removal release, whereas standard error on a container is a place messages go to die. Set the action to `default` rather than `always` in a high-traffic process. `always` re-emits per occurrence and a warning inside a per-request path will flood the pipeline; `default` shows each source location once, which is the information content you actually need - the same line warning ten million times teaches you nothing more than it warning once. If the volume still matters, an aggregation counter around a `once` action is the next step. ### Set it before your code runs A policy installed in `main()` is already too late for anything emitted during import - and a surprising amount is. This bites hardest during exactly the kind of change that reshuffles imports: a team untangling a circular import at startup moves module-level work around, and the deprecations firing during import are attributed and filtered before the entry point executes, so a filter installed there never sees them. Configure through `PYTHONWARNINGS` in the process environment, or `-W` on the command line, so the machinery is configured before the first import. Keep runtime `filterwarnings` calls for the narrow cases where the decision genuinely depends on runtime state. One related constraint: because the filter list is process-global with no locking, a service that mutates filters at runtime while serving concurrent work has a race on its own configuration. Another reason to set it once and leave it alone. ### Your own deprecations Policy is not only about consuming warnings. For code you own and other teams call: - Emit with `stacklevel=2` so the message names the caller's line, and so it is attributed to a module the caller's filters can select. - Define one warning base class per library so consumers can silence you without silencing everything - a blanket `ignore` is what you get otherwise, and it silences the next deprecation too. - Give a removal version in the message text, so the reader knows the deadline without opening a changelog. - Since Python 3.13, `warnings.deprecated` marks a function or class as deprecated for both the runtime and type checkers, so a static check can surface it before anything runs. ### What good looks like Deprecations fail the build; production counts them and alerts on a rising rate; the exemption list is short, pinned and dated; and a warning emitted by your own code names the caller's line and its removal release. The failure mode this prevents is the ordinary one: a dependency upgrade that removes a function which had been warning, unheard, for two minor versions.

  • Why not run the production service with the same fatal warning policy as CI?
    Because nobody on call can fix a deprecated call, and promoting a warning to an exception turns a cosmetic problem into failed scoring requests. Worse, the promotion fires at an arbitrary code path under real traffic, so the blast radius is unpredictable. Production's job is to make the signal visible and counted; CI's job is to make it blocking.
  • How do you keep a list of ignore filters from becoming permanent silence?
    Pin each entry to a specific message pattern and module rather than a whole category, so an unrelated deprecation from the same package still fails. Keep the list in one reviewed file with an owner, an upstream issue and a review date per line, and treat its length as a tracked metric. A blanket `ignore::DeprecationWarning` has no natural moment at which anyone revisits it.
  • Why prefer the default action over always for warnings in a high-traffic service?
    `always` emits on every occurrence, so a warning on a per-request path floods the log pipeline and costs real money without adding information. `default` reports each source location once, which is exactly the fact you need: which line must change. If you need volume data, count occurrences with a metric rather than by printing each one.
  • Where should the filter configuration live so nothing is missed?
    In the process environment or on the command line - `PYTHONWARNINGS` or `-W` - so the machinery is configured before the first import. Warnings emitted while modules are being imported are filtered before an entry point ever runs, so a policy installed in `main()` silently misses them. Runtime `filterwarnings` calls should be reserved for decisions that genuinely depend on runtime state.

saying these in an interview costs you the question

  • Runs production with warnings promoted to errors
  • Adds a blanket ignore for DeprecationWarning to quiet CI
  • Configures filters in main() and misses import-time warnings
  • Leaves warnings on stderr with no aggregation in production
  • Uses the always action on a per-request code path
  • Treats warning policy as a per-developer preference, not a shared setting

context