skip to content

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