The warnings.catch_warnings context manager gives Python applications temporary control over warnings in tests, libraries, migration scripts, and legacy integrations. Instead of changing warning filters permanently, it saves the current state, applies rules inside a block, and restores the previous configuration when the block ends. This makes it useful for capturing, converting, ignoring, or validating warnings without affecting unrelated code.
Warnings are not exceptions. They report situations that deserve attention, such as deprecated APIs, behavior scheduled to change, possible precision loss, or questionable resource usage. Execution normally continues. Mature tests should therefore verify not only return values but also the warnings produced by an operation.
Basic usage
import warnings
with warnings.catch_warnings(record=True) as captured:
warnings.simplefilter("always")
warnings.warn("old feature", DeprecationWarning)
assert len(captured) == 1
assert captured[0].category is DeprecationWarning
assert "old feature" in str(captured[0].message)
With record=True, the context returns a list of WarningMessage objects. Each entry contains the message, category, file name, line number, and other details. The always filter is important in tests because Python may otherwise suppress repeated warnings through its internal warning registry.
Why state restoration matters
Calls such as warnings.simplefilter and warnings.filterwarnings modify warning state. If one test ignores every warning and fails to restore the configuration, later tests can pass incorrectly. The context manager reduces that risk by restoring filters even if an exception leaves the block.
Restoration does not mean every use is automatically concurrency-safe. Warning configuration can involve shared state, and behavior should be checked against the Python version used by the application. In multithreaded or asynchronous programs, avoid wide contexts while unrelated execution changes filters. Prefer short blocks, isolated tests, and centralized startup policies.
Capturing one category
import warnings
def legacy_api():
warnings.warn(
"legacy_api will be removed",
DeprecationWarning,
stacklevel=2,
)
return 42
with warnings.catch_warnings(record=True) as captured:
warnings.simplefilter("always", DeprecationWarning)
result = legacy_api()
assert result == 42
assert len(captured) == 1
assert issubclass(captured[0].category, DeprecationWarning)
Filtering by category is safer than hiding every warning. A broad ignore rule can conceal ResourceWarning, RuntimeWarning, or important dependency notices. A narrow rule communicates the test’s intention and reduces accidental suppression.
Turning warnings into errors
During testing and continuous integration, treating selected warnings as exceptions can help teams remove deprecated APIs before upgrades break production.
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
legacy_api()
The call now raises DeprecationWarning as an exception. This policy works well in controlled test suites, but applying it indiscriminately in production can stop requests because of third-party library warnings. Start with selected categories and modules.
Filtering by message and module
filterwarnings supports detailed criteria including action, a regular expression for the message, warning category, module expression, and line number.
with warnings.catch_warnings(record=True) as captured:
warnings.filterwarnings(
"always",
message=r".*old parameter.*",
category=DeprecationWarning,
module=r"my_package\..*",
)
run_flow()
Keep expressions simple and stable. Tests tied to the complete wording of a dependency can fail after harmless editorial changes. Prefer checking the category, a meaningful fragment, and the source.
The role of stacklevel
Library authors should set stacklevel so the warning points to the caller’s code rather than the internal line that invokes warnings.warn. A value of two commonly points one frame above, although wrappers may require a larger number.
def new_name():
return 10
def old_name():
warnings.warn(
"use new_name()",
DeprecationWarning,
stacklevel=2,
)
return new_name()
A correctly located warning reduces investigation time. Tests can validate filename and lineno when source location is part of the public contract.
Warning registries
Python maintains registries to avoid showing certain messages repeatedly. This explains why a warning can appear once and disappear in later calls. In tests, simplefilter("always") inside catch_warnings makes behavior more deterministic. Imported modules may still have their own registries, so suites should avoid relying on execution order.
Integration with logging
logging.captureWarnings(True) redirects warnings to Python’s logging system. This is useful for services, but it differs from collecting a list for assertions. Unit tests often benefit from catch_warnings(record=True), while production logging provides timestamps, correlation information, and centralized destinations.
Good practices for library authors
Choose categories deliberately. DeprecationWarning is commonly used for developer-facing removal notices, while FutureWarning may fit changes that directly affect end users. Custom categories can separate domain-specific notices. Document when the warning was introduced, the recommended replacement, and the planned removal window.
Do not use a warning when the result is invalid; raise an exception instead. Avoid emitting the same warning repeatedly inside hot loops, because it creates noise and additional overhead.
Testing for unexpected warnings
with warnings.catch_warnings(record=True) as captured:
warnings.simplefilter("always")
run_stable_operation()
unexpected = [w for w in captured if w.category is not UserWarning]
assert not unexpected
This pattern accepts a known category while rejecting others. Large projects can wrap it in reusable helpers that provide clearer failure messages and consistent filtering.
Concurrency and scope
The main operational risk is assuming filters are local while another execution path changes the same warning state. Keep contexts short and avoid surrounding long network calls, sleeps, or parallel processing. In servers, define the broad warning policy at startup and reserve temporary capture mostly for tests and controlled jobs.
When to use catch_warnings
Use it to test deprecations, validate library behavior, silence one known warning in a narrow section, convert selected categories into errors, or inspect message and origin. Do not use it merely to make a terminal look clean. Repeated warnings usually indicate an outdated dependency, old API, or behavior that needs correction.
For more information, read the official warnings documentation and the section about logging integration. On Academify, continue with the sections about Python, Python testing, programming, and the Python course.
Conclusion
warnings.catch_warnings provides temporary and testable control over Python warnings. Reliable usage combines specific filters, short scopes, record=True for assertions, correct stacklevel values when emitting notices, and caution around shared state. With these practices, warnings become useful signals for quality, compatibility, and maintenance instead of background noise.







