How does Python choose which entry in warnings.filters applies to a warning?
answer
- It is a list, not a set
- Order of the list decides
- The scan stops at the first hit
- New entries go in at the front
- append=True lands behind the defaults
basics
~20 sPython walks warnings.filters from the front and stops at the first entry whose message, category and module all match; that entry's action decides the outcome. filterwarnings and simplefilter insert at the front, so the newest entry is checked first.
solid answer
~40 s`warnings.filters` is an ordered list of `(action, message, category, module, lineno)` tuples. The machinery walks it from index 0 and stops at the **first** entry whose message regex, category subclass check, module regex and line number all match; that entry's action decides the outcome, nothing below it is consulted, and nothing is merged. Both `warnings.filterwarnings` and `warnings.simplefilter` insert at the *front* by default, so the most recently registered filter is consulted first. `append=True` instead puts an entry at the end, behind the `ignore` entry for `DeprecationWarning` that CPython installs at startup — which is why an appended `error` filter looks inert. `-W` and `PYTHONWARNINGS` entries are applied during startup, each also inserted at the front, so they sit above the five defaults but below anything registered at runtime.
code
python · 9 linesimport warnings
warnings.filterwarnings("error", category=DeprecationWarning) # registered first
warnings.filterwarnings("ignore", category=DeprecationWarning) # registered second
print(warnings.filters[0])
print(warnings.filters[1])
warnings.warn("old_api is deprecated", DeprecationWarning)
print("no exception: the 'ignore' entry matched first and ended the search")go deeper
Recall that warnings are not controlled by one global on/off switch: warnings.filters is an ordered list of rules. Printing it in a REPL and reading the tuples is a legitimate first move when a warning surprises you.
Be ready to name the five fields of a filter entry and explain that the list is scanned front to back with the first match ending the search. Know that filterwarnings and simplefilter insert at the front, and that append=True does not.
Show you can debug a filter that never fires in a real suite: print the live filter list at the moment of the warning, look for a later registration shadowing yours, and remember the module field is matched against a file path, not an import name.
Own the ordering contract across a codebase: where the floor is set at startup versus what libraries may register at import time. Because the last registration is consulted first, uncoordinated global filter edits become a process-wide ordering contest, and that is a standard to set.
`warnings.filters` is a plain list, and every decision the warnings machinery makes about a single `warnings.warn` call comes from walking that list once, front to back, and **stopping at the first entry that matches**. Nothing is merged, nothing is aggregated, and there is no notion of "the strictest rule wins". That one sentence explains almost every "my filter did not fire" report. ## What an entry looks like Each element of `warnings.filters` is a five-tuple `(action, message, category, module, lineno)`. `action` is the outcome — one of `"error"`, `"ignore"`, `"always"` (also spelled `"all"`), `"default"`, `"module"` or `"once"`. The other four are the match conditions: - **`message`** is a compiled regular expression tested against the warning's message text from the start, and `warnings.filterwarnings` compiles it case-insensitively; `None` means "match any message". - **`category`** matches when the warning's class is a subclass of it, which is why an entry for `Warning` catches everything and an entry for `DeprecationWarning` also catches its subclasses. - **`module`** is another regex anchored at the start, this one case-sensitive; `None` matches any module. - **`lineno`** matches when it is `0` or exactly equal to the line the warning came from. ## What those conditions are matched against The message text and the category come straight from the `warnings.warn` call. The module string, crucially, is *not* the dotted import name: the machinery derives it from the source filename by stripping a trailing `.py`, so a warning raised in a file at `/opt/app/lib2.py` is matched against the string `/opt/app/lib2`. An entry written as `module="pkg.client"` is anchored at the start of that path and can never match it. This is the most common reason a carefully-written filter is silently inert — write `.*client`, or drop the module condition and constrain by category and message instead. ## How the list gets built, and in what order 1. At interpreter start CPython installs five default entries: `default` for `DeprecationWarning` raised in `__main__`, then `ignore` for `DeprecationWarning`, `PendingDeprecationWarning`, `ImportWarning` and `ResourceWarning`. 2. Each `-W` option and each `PYTHONWARNINGS` entry is then applied in order, and every one of them is *inserted at the front*, so a later option outranks an earlier one and all of them outrank the defaults. 3. At runtime `warnings.filterwarnings` and `warnings.simplefilter` insert at the front too. That produces the rule people most often have backwards: **the last filter registered is the first one consulted.** Whoever runs latest wins, which makes import order, test-session setup and any third-party code that touches filters part of the answer. ## So why does `warnings.filterwarnings("error", category=DeprecationWarning)` in a test module still merely print? Work down the causes. - (1) It was registered with `append=True`, which puts it at the *end*, behind the shipped `ignore` entry for `DeprecationWarning` that matches the same warning first — the appended entry is never reached. - (2) Something registered a broader entry afterwards; because insertion is at the front, a later `warnings.simplefilter("ignore")` anywhere in the process shadows yours entirely. - (3) A `warnings.catch_warnings` block saved the list and restored it on exit, discarding a filter registered inside it. - (4) `warnings.resetwarnings()` cleared the list. - (5) The `message` or `module` regex does not match what the machinery actually sees. The diagnostic is identical in every case: print `warnings.filters` immediately before the call that should warn and read it top to bottom yourself — the first entry whose four conditions all hold is the one deciding the outcome, and everything below it is dead code. ## One mechanism worth not confusing with this one Matching decides *which action applies*. The `"once"`, `"module"` and `"default"` actions then additionally consult a **per-location record**, so a repeated warning from the same place prints only the first time. If your filter matched and yielded `default` but you see the message just once, that suppression is the per-location record, not the filter chain — a different question with a different fix. ## Practical shape - **Set the floor** on the command line (`-W error::DeprecationWarning`) or in `PYTHONWARNINGS` so it is installed before any import runs, and remember it is only a floor: any runtime `warnings.filterwarnings` call lands in front of it. - **Reach for `append=True` deliberately** and only when you want a backstop that existing entries are allowed to override — a library supplying a default without overruling the application's policy. - **Keep runtime filter edits inside a scope you control** rather than mutating global state at library import time, because the front-insertion rule turns global filter edits into an ordering contest nobody wins.
- Your filter passes module="mypkg.client" and never fires. Why?Because the module field is not the dotted import name. The warnings machinery derives that string from the source filename by stripping a trailing `.py`, so it sees something like `/opt/venv/lib/python3.14/site-packages/mypkg/client`. A regex anchored at the start with `mypkg.client` cannot match that. Use `.*client`, or drop the module condition and constrain by category and message instead.
- Where do -W and PYTHONWARNINGS entries sit relative to a filterwarnings call made at runtime?Underneath it. Startup options are applied before user code runs, each inserted at the front, so they outrank the five default entries. But any `warnings.filterwarnings` or `warnings.simplefilter` call during execution inserts ahead of all of them. A command-line option is a floor, not a lock: application or library code can still shadow it.
- When is append=True actually the right choice?When you want a backstop that existing entries are allowed to override — a library supplying a default without overruling the application's policy, for instance. It is the wrong choice in a test that must make a deprecation fatal, because the shipped `ignore` entry for `DeprecationWarning` sits ahead of anything appended and matches first.
The filter list behaves like a firewall rule chain: rules are tried top to bottom, the first one that matches decides the outcome, and every rule below it is never consulted. Adding a rule at the bottom of a chain that already matches changes nothing.
saying these in an interview costs you the question
- Thinks matching filters are merged, strictest action winning
- Assumes the last filter registered is consulted last
- Expects append=True to override the default filters
- Writes the module field as a dotted import name
- Believes a -W option cannot be shadowed at runtime
- Thinks filterwarnings replaces the list instead of inserting