Skip to content
← All Python articles

Python explainer

Warn Callers with the warnings Module

About 5 min to read. Allow extra time to work through examples.

Tell the people who call your code about a problem without stopping their program, point the warning at their line, and turn warnings into errors in your tests.

This article uses Python. You can make it your practice mode to browse related content across the site.

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

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.

step 15 of 20

Error Handling with Exceptions, Retries and Warnings

Up next Deprecate a Function Without Breaking Callers