skip to content

Runtime Check Switches

Switches that make the interpreter tell you more before anything breaks: development mode and the warning filters, and what each one costs to leave on in a service.

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

questions

8

How does warnings.warn() differ from raising an exception in Python?

level: juniorimportance: must knowfreq 45%

answer

  1. Execution does not stop
  2. It is not a print call
  3. A rule list is consulted first
  4. One call can print, hide or raise
  5. warnings.filters and the error action

basics

~20 s

warnings.warn() reports a problem without stopping execution: the call returns and the code keeps running. It also passes through the filter list in warnings.filters first, so the same call may print, be silenced, or be turned into a raised exception.

solid answer

~40 s

A raised exception unwinds the stack immediately and the caller must handle it. `warnings.warn(message, category)` instead hands the message to the warnings machinery and returns, so execution continues. Before anything is shown, the message is matched against `warnings.filters`, an ordered list of rules; the first matching rule decides the action, which may be `default` (print once per source location), `always`, `once`, `module`, `ignore` or `error`. Only `error` actually raises, and that is what `-W error` or `PYTHONWARNINGS=error` turns on. Whatever gets through is rendered by `warnings.showwarning` and written to standard error, not to a logger. Use a warning for something the caller should change but that does not break this call - a deprecated argument, a resource left open; raise when the call genuinely cannot proceed.

code

python · 9 lines
python
import warnings

def load_rules(path):
    warnings.warn("load_rules() is deprecated; use load_ruleset()", DeprecationWarning)
    return path

for _ in range(3):
    load_rules("rules.json")
print("still running")

go deeper

for a junior

Be ready to say plainly that warnings.warn() returns and the next line runs, while raise does not. Know the default category is UserWarning and that the text goes to standard error.

for a middle

Explain the filter list between the call and the output: the actions ignore, default, always, once and error, and why the same call can print, vanish or raise depending on how the process was started.

for a senior

Show judgement about which channel a given condition belongs to, and be able to say why a deprecation that only warns in development is worthless if nothing in CI promotes it to an error.

for a principal

Own the convention across a codebase: a per-library warning base class so callers can silence you selectively, and a stated rule for when a soft deprecation becomes a hard exception in a release.

### Two different channels Python has three ways to report that something is wrong, and they are not interchangeable. An **exception** says *this call cannot continue*: `raise` unwinds the stack until a handler catches it, and the code after the `raise` never runs. A **log record** says *here is an event, for whoever is reading the logs later*. A **warning** sits between them: it says *this code ran, but the way you called it is wrong or will stop working*, and it is aimed at the developer of the calling code rather than at an operator watching a dashboard. `warnings.warn("message", SomeWarningCategory)` returns `None` and execution continues on the next line. That is the whole behavioural difference from `raise`, and it is why a warning is the right tool for a deprecation: the old API still works today, so breaking the caller now would be wrong, but the caller needs to hear about it. ### What actually happens inside warn() The call is not a `print`. Four things happen in order. 1. **A category is chosen.** The second argument defaults to `UserWarning`. Other built-in categories include `DeprecationWarning`, `PendingDeprecationWarning`, `RuntimeWarning`, `SyntaxWarning`, `ResourceWarning`, `ImportWarning`, `FutureWarning` and `BytesWarning`. The category is a class, and filters match on it by subclass, so a filter for `Warning` matches everything. 2. **A source location is attributed.** The machinery walks up the call stack by `stacklevel` frames (default 1, meaning the line that called `warn`) and records that file, line number and module name. That location is used both for the printed `file:line:` prefix and for filter matching. 3. **The filters are consulted.** `warnings.filters` is an ordered list of tuples `(action, message-regex, category, module-regex, lineno)`. The machinery walks it top to bottom and the **first** match wins; if none match, the default action applies. The actions are: - `error` - raise the category as an exception, at the point of the `warn()` call - `ignore` - drop it silently - `always` - print every time - `default` - print once per unique (message, category, source location) - `module` - print once per module - `once` - print once per process for that message and category 4. **It is rendered and written.** If the action is not `ignore` or `error`, `warnings.formatwarning` builds the text and `warnings.showwarning` writes it to `sys.stderr`. Neither the `logging` module nor any handler is involved unless you explicitly call `logging.captureWarnings(True)`. ### The surprise everyone hits Because the default action is `default`, a warning inside a loop prints **once**, not once per iteration - the machinery keeps a per-module record of the (message, category, line) triples it has already shown. Beginners read the single line, assume it happened once, and go looking for a bug that is not there. Ask for `always` if you want every occurrence. ### try/except does not catch a warning A warning is not a raised event, so `try: warnings.warn(...) except UserWarning:` catches nothing - the `except` clause simply never runs. It becomes catchable only if a filter with the `error` action promoted it, which is exactly why `-W error` is the standard way to make a test suite fail on deprecations: the promotion turns a soft signal into a normal exception that existing test machinery already understands. ### Choosing between the three - **Raise** when the operation cannot produce a correct result: bad argument, missing file, broken invariant. - **Warn** when the operation succeeded but the caller's *code* needs to change: a deprecated function, a default that is about to flip, a file that was garbage-collected while still open (`ResourceWarning`). - **Log** when the audience is an operator rather than a developer, and when the message is about this run rather than about the source code. A useful test: if the fix is to edit source code, warn; if the fix is to look at production state, log; if there is no sensible way to continue, raise. ### Custom categories Subclass `Warning` (or `UserWarning`) for your own project so that callers can filter your warnings without silencing everything else. A shared base class per library is what makes a filter such as `ignore::MyLibWarning` possible, and it is the difference between a caller silencing your noise and a caller silencing all noise.

  • If warnings.warn() does not raise, can you write try/except around it to catch the warning?
    Not by default - nothing is raised, so the `except` clause never runs. A warning becomes catchable only when a filter with the `error` action promotes it, at which point the category class is raised at the `warn()` call site like any other exception. To observe warnings without promoting them, use `warnings.catch_warnings(record=True)` instead.
  • Why does the same warnings.warn() call print only once even though the line runs in a loop?
    The default filter action is `default`, which shows a given message, category and source location only the first time. The machinery keeps a per-module record of what it has already shown. Set the `always` action - via `warnings.simplefilter("always")` or `-W always` - if you need every occurrence.
  • When would you use logging.Logger.warning instead of warnings.warn?
    When the audience is an operator rather than a developer. `logging` reports events about this run and flows into your handlers and aggregation; `warnings.warn` reports that the *source code calling you* needs to change, is deduplicated per source location, and goes to standard error. Deprecations and misuse belong in `warnings`; runtime conditions belong in `logging`.

saying these in an interview costs you the question

  • Thinks warnings.warn raises an exception and aborts the call
  • Believes every warn() call prints on every execution
  • Uses print() to stderr instead of the warnings channel
  • Thinks try/except catches a warning that was never promoted
  • Confuses warnings.warn with logging.Logger.warning
  • Assumes warnings go through logging handlers by default

context

open as a page

Why does a DeprecationWarning from an imported library not print by default?

level: middleimportance: must knowfreq 55%

basics

~10 s

Python ships default entries in warnings.filters that ignore DeprecationWarning everywhere except when it is triggered from the main module. Surface them with -W default::DeprecationWarning, PYTHONWARNINGS, or a warnings.filterwarnings call.

open as a page

What does running CPython with the `-X dev` switch turn on?

level: juniorimportance: should knowfreq 30%

basics

~20 s

Development mode makes a normal interpreter noisy. It adds the default warning filter so ignored categories like ResourceWarning print, installs debug hooks around memory allocations, enables faulthandler and asyncio debug mode, and sets sys.flags.dev_mode to True.

open as a page

Why does `ResourceWarning: unclosed file` appear only under `-X dev`?

level: middleimportance: should knowfreq 35%

basics

~20 s

ResourceWarning is discarded by CPython's default warning filters, so nothing prints. Development mode adds the default filter, which shows it. The warning itself is emitted by the file object's finalizer when it is garbage-collected without being closed.

open as a page

What does the stacklevel argument to warnings.warn() control?

level: middleimportance: should knowfreq 32%

basics

~20 s

stacklevel picks which stack frame the warning is blamed on. The default 1 points at the warn() call itself; 2 points at that function's caller, which is what a deprecated API should report so the user sees their own line.

open as a page

How do you decide whether to leave `PYTHONDEVMODE=1` on for a 6-hour nightly video-metadata extraction job?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Weigh the extra checks against whether anyone reads the output. A long offline batch job is a good candidate: latency does not matter and slow handle leaks surface over hours. Measure the slowdown first, and route warnings to a reader.

open as a page

How does warnings.catch_warnings isolate filter changes inside a test?

level: seniorimportance: should knowfreq 38%

basics

~20 s

warnings.catch_warnings is a context manager that snapshots warnings.filters and the display hook on entry and restores both on exit, so filters you install inside the block cannot leak into later code. With record=True it also collects the warnings raised inside.

open as a page

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

level: principalimportance: should knowfreq 26%

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.

open as a page