Python Basics: Capturing and Filtering warnings
A function may still work while using an option that callers should replace. A warning communicates that condition without necessarily stopping execution. Python’s warnings module lets callers capture that signal, suppress a specific known case, or treat it as an exception.
These examples deliberately capture warnings instead of showing Python’s normal filename-and-line-number display. That keeps the output stable and makes it possible to assert exactly which warning was emitted.
Prepare a scratch directory
These three standalone examples were tested on Linux with Python 3.12.14. Use Bash, Python 3.12 or newer, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Keep assertions enabled by running without Python’s -O option.
Create a fresh directory and keep using the same terminal:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Save each named source file here as UTF-8. Avoid naming a source file warnings.py. The examples are single-threaded and change warning filters only inside catch_warnings contexts.
1. Test a warning as structured data
Save as capture_option_warning.py:
import warnings
class LegacyOptionWarning(UserWarning):
pass
def choose_format():
warnings.warn("Use format='json' instead",
LegacyOptionWarning, stacklevel=2)
return "json"
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always", LegacyOptionWarning)
result = choose_format()
assert len(caught) == 1
assert caught[0].category is LegacyOptionWarning
assert str(caught[0].message) == "Use format='json' instead"
assert result == "json"
print("Result:", result)
print("Count:", len(caught))
print("Category:", caught[0].category.__name__)
print("Message:", caught[0].message)Run it:
python3 capture_option_warning.pyOutput:
Result: json
Count: 1
Category: LegacyOptionWarning
Message: Use format='json' insteadThe custom category distinguishes this condition from unrelated warnings. The function still returns json under the always filter. With record=True, the context collects warning records instead of writing the normal warning display.
stacklevel=2 points a normal display at the caller rather than the warn call inside the helper. The assertions inspect the category and text separately; they do not depend on where the scratch directory happens to be.
2. Ignore only one known message
Save as filter_known_warning.py:
import warnings
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always", UserWarning)
warnings.filterwarnings(
"ignore", message=r"^Optional preview unavailable$",
category=UserWarning)
warnings.warn("Optional preview unavailable", UserWarning)
warnings.warn("Export directory is unset", UserWarning)
messages = [str(item.message) for item in caught]
assert messages == ["Export directory is unset"]
print("Captured count:", len(caught))
print("Still visible:", messages[0])Run it:
python3 filter_known_warning.pyOutput:
Captured count: 1
Still visible: Export directory is unsetThe narrow ignore rule is added after the general always rule. New filters go at the front by default, and the first matching rule wins. The anchored expression matches the whole known message. It leaves the second warning available for inspection.
This is more deliberate than hiding every warning from a package. Keep such a suppression close to the operation that needs it, explain why it is acceptable, and remove it when the underlying condition is fixed.
3. Make a warning fail a check
Save as raise_legacy_warning.py:
import warnings
class LegacyOptionWarning(UserWarning):
pass
def normalize(mode):
if mode == "old":
warnings.warn("Use the new mode", LegacyOptionWarning,
stacklevel=2)
return mode.upper()
with warnings.catch_warnings():
warnings.simplefilter("error", LegacyOptionWarning)
print("Supported mode:", normalize("new"))
try:
normalize("old")
except LegacyOptionWarning as error:
print("Rejected:", error)
else:
raise AssertionError("The warning did not raise")
print("Strict check finished")Run it:
python3 raise_legacy_warning.pyOutput:
Supported mode: NEW
Rejected: Use the new mode
Strict check finishedUnder the error filter, warn raises the category as an exception. The old path stops before reaching return. Catching that exact category proves the strict check worked, while the else branch would expose an unexpected success.
Warnings are useful for migrations and recoverable conditions. Invalid input that must never proceed should normally raise an ordinary exception directly, rather than relying on a caller’s warning settings.
Leaving catch_warnings restores the previous filter configuration. In Python 3.12 it changes global state and is not thread-safe, so do not overlap these contexts in multiple threads. Default visibility also varies by category; explicitly set the intended filter when testing.
Clean up
In the same terminal, remove only these source files, return to the original directory, and remove the now-empty scratch directory:
rm -- capture_option_warning.py filter_known_warning.py raise_legacy_warning.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf rmdir reports that anything remains, inspect the directory rather than deleting unknown files.
References
Acknowledgements
This post was created with assistance from AI.