Your library has a function, old_label(), that you plan to remove. Programs
call it today, and you do not want to break them. You want to tell their
authors which line to change. warnings.warn does that. Here is your
library's module, labels.py:
import warnings
def old_label(value: int) -> str:
warnings.warn(
"old_label() is deprecated, use str() instead",
DeprecationWarning,
stacklevel=2,
)
return str(value)
And here is app.py, a script that uses it:
from labels import old_label
print(old_label(7))
Run python app.py. Python prints the warning and keeps running:
/home/you/app.py:3: DeprecationWarning: old_label() is deprecated, use str() instead
print(old_label(7))
7
A warning is a message about a problem that is not an error yet. The program keeps running, and the caller decides whether to show the message, hide it, or turn it into an exception.
Point the warning at the caller's line
stacklevel says which line the warning names. The default, stacklevel=1,
names the line that calls warnings.warn, inside labels.py. With
stacklevel=2, it names the line one step up the call stack, the call in
app.py. That is the line the user has to change.
stacklevel also decides whether the user sees the warning at all. Python hides a
DeprecationWarning unless the line it names is in __main__, the script you
ran. With stacklevel=1, the warning above names a line in labels.py, so
python app.py prints only 7.
If old_label calls a helper and the helper warns, the helper needs
stacklevel=3. Add one for each function between the warning and the caller.
Pick a category
The second argument to warnings.warn is the warning's category, a class
that says what kind of warning it is. Filters use the category to decide what
to do.
| Category | Use it for |
|---|---|
DeprecationWarning |
A feature you will remove, reported to developers who call your code |
FutureWarning |
The same, reported to the people who use an application |
UserWarning |
Anything else. It is the default when you pass no category |
Python shows FutureWarning by default and hides DeprecationWarning outside
__main__. Test runners such as unittest and pytest show both, so the
developers who call your code see your warning when they run their tests.
Filters decide what happens
A filter is a rule that matches warnings by category, message or module, and says what to do with them. The main actions are:
default: print a warning the first time it comes from each line of code.always: print it every time.ignore: print nothing.error: raise the warning as an exception.
The quickest way to add a filter is Python's -W option. This command stops
the run with an exception at the first deprecated call:
python -W error::DeprecationWarning app.py
The part after -W is the action, an empty message pattern that matches any
message, and the category. pytest takes the same option, so use it in CI to
find the deprecated calls your tests reach. In code,
warnings.simplefilter("error", DeprecationWarning) adds the same filter.
Python puts each new filter at the front of its list and uses the first one
that matches.
warnings.catch_warnings() is a context manager. In a with statement, it
saves the filters and puts them back when the block ends. With record=True, it also collects the
warnings in a list instead of printing them. That is how you test that your
code warns:
import warnings
def old_label(value: int) -> str:
warnings.warn(
"old_label() is deprecated, use str() instead",
DeprecationWarning,
stacklevel=2,
)
return str(value)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
assert old_label(7) == "7"
assert len(caught) == 1
assert caught[0].category is DeprecationWarning
assert "use str()" in str(caught[0].message)
The always filter makes catch_warnings record the warning even if an
earlier call from the same line already printed it. In pytest,
pytest.warns(DeprecationWarning) does the same job.
Leave the filters to the application
A library calls warnings.warn and leaves the filters to the application and
its tests. A filter that a library adds at import time also changes what
happens to every other library's warnings.
Check your understanding
A user never sees the DeprecationWarning from your library, which calls warnings.warn(message, DeprecationWarning). What do you change?
Show answer
Hide answer
Add stacklevel=2. Without it, the warning names a line inside your library.
That line is not in __main__, so Python's default filters hide the warning.
With stacklevel=2, the warning names the user's own line, and Python shows
it when that line is in the script they ran.
Going further
On Python 3.12 and 3.13, catch_warnings changes the filters for the whole
process, so other threads see them too. Python 3.14 added an opt-in mode
that keeps the changes inside the current thread or asyncio task. Turn it on
with -X context_aware_warnings=1, and check it with
sys.flags.context_aware_warnings. It is off by default, except in the
free-threaded build. The track Concurrency with Threads and asyncio covers
threads and tasks.
The warnings documentation
lists every category and filter option.