Warnings & Diagnostics
Python's warnings module flags conditions worth noticing without stopping the program, while environment variables and debug flags surface deeper diagnostic information for tracking down subtle bugs.
Not every problem deserves a crash. The standard library's warnings module lets code signal 'this is deprecated' or 'this input is suspicious' without raising an exception, while a family of debug flags and diagnostic tools (development mode, tracing, environment variables) give visibility into what the interpreter is doing beneath the surface when a bug is hard to pin down through normal debugging alone.
What Warnings Are and Why They're Not Exceptions
A warning reports a condition that's probably a mistake or will become one (a deprecated API, a resource left unclosed, a questionable numeric operation) without halting execution, unlike an exception which represents a definite error. Warnings let a library evolve — flagging that a feature will be removed in a future version — while existing code keeps running today.
The warnings Module: Basic Usage
warnings.warn() issues a warning, which by default is printed to stderr once per unique location, without stopping the program.
stacklevel=2 makes the warning point at old_function's caller rather than the line inside old_function itself, which is almost always what you want for a deprecation notice.
import warnings
def old_function():
warnings.warn(
"old_function is deprecated, use new_function instead",
DeprecationWarning,
stacklevel=2,
)
return new_function()
def new_function():
return 42
old_function()app.py:10: DeprecationWarning: old_function is deprecated, use new_function instead
old_function()Warning Categories
Warnings are classified by category (a class hierarchy rooted at the built-in Warning class), which lets tools filter by kind.
DeprecationWarning is ignored by default in most contexts except when the code triggering it is being run directly (e.g. __main__) or under test runners, since it's aimed at developers, not end users of an application; FutureWarning is shown by default because it targets end users.
Warning base class for all warnings
UserWarning default category if none is specified
DeprecationWarning a feature is deprecated (visible to developers, often hidden for end users)
PendingDeprecationWarning a feature will be deprecated in a future version
SyntaxWarning questionable syntax
RuntimeWarning a questionable runtime behavior (e.g. certain numeric edge cases)
FutureWarning behavior will change in a future release; intended for end users, not just developers
ResourceWarning a resource (file, socket) wasn't cleaned up properlyCustom Warning Categories
You can define a project-specific warning category by subclassing Warning, which lets consumers of your library filter your warnings independently of built-in ones.
class ConfigWarning(UserWarning):
pass
warnings.warn("unknown config key 'legacy_mode' ignored", ConfigWarning)Filtering Warnings
The warnings filter controls what happens when a warning is issued: ignore it, print it once, print it every time, turn it into an exception, or print it only the first time from each location. Filters can be set programmatically or via environment/command-line configuration.
Turning DeprecationWarning into an error is a common and effective pattern in a test suite: it catches use of deprecated APIs before they become failures in a future dependency upgrade.
import warnings
warnings.filterwarnings("error", category=DeprecationWarning) # escalate to an exception
warnings.filterwarnings("ignore", category=ResourceWarning) # silence entirely
warnings.filterwarnings("default", category=UserWarning) # print once per location (default behavior)python -W error::DeprecationWarning app.py # command-line equivalent
PYTHONWARNINGS="error::DeprecationWarning" python app.py # environment variable equivalentSuppressing Warnings Locally
warnings.catch_warnings() as a context manager scopes a filter change to a specific block, restoring the previous filter state afterward — useful for silencing a known, deliberate warning without affecting the rest of the program.
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
old_function() # warning suppressed only within this blockDebug Flags and Environment Configuration
Beyond warnings, CPython exposes several environment variables and command-line flags that change diagnostic behavior without modifying code.
PYTHONWARNINGS set default warning filters (as above)
PYTHONDEBUG / -d enable parser debug output (rarely needed directly)
PYTHONFAULTHANDLER=1 enable faulthandler at startup (see below)
PYTHONASYNCIODEBUG=1 enable asyncio's debug mode (detects slow callbacks, unawaited coroutines)
PYTHONTRACEMALLOC=1 start tracemalloc at interpreter startup, tracking memory allocations
-X dev enable Python's "development mode" (see next section)Development Mode (-X dev)
Python's development mode, enabled with python -X dev script.py or PYTHONDEVMODE=1, turns on a bundle of extra runtime checks useful while developing: it shows DeprecationWarning and ResourceWarning by default, enables faulthandler, enables asyncio debug mode, and adds extra runtime checks (like flagging use of an uninitialized memory in some C-level operations). It intentionally trades some performance for better diagnostics and should not be enabled in production.
python -X dev app.pyTracing Execution
The standard library's trace module can run a script while recording which lines or functions actually executed, useful for coverage-style analysis or understanding an unfamiliar codebase's actual runtime path. At a lower level, sys.settrace() installs a trace function called on every line/call/return event, which is the same mechanism pdb itself is built on.
For real coverage measurement, the third-party coverage.py tool (often used via pytest-cov) is far more practical than the stdlib trace module; trace is useful mainly for quick, dependency-free diagnostics.
python -m trace --trace app.py # print every line as it executes
python -m trace --count app.py # produce per-line execution counts (coverage-like)faulthandler: Diagnosing Crashes and Hangs
The standard library's faulthandler module dumps a Python traceback automatically on a segmentation fault, abort, or other fatal low-level error — cases where the interpreter can't produce a normal traceback on its own. It's also useful for diagnosing a hung process by registering a signal that dumps the current stack on demand.
import faulthandler
faulthandler.enable()
# on a crash that would otherwise be silent (e.g. a segfault in a C extension),
# faulthandler prints the Python-level traceback to stderr before the process diesPractical Usage and Best Practices
Use DeprecationWarning (with stacklevel set correctly) when planning to remove or change a public API, giving users advance notice. Turn warnings into errors during testing (pytest has a built-in -W error option, or filterwarnings markers) to catch deprecated usage early. Reach for -X dev during local development, not in production, since its extra checks carry real overhead. Use tracing/faulthandler sparingly and specifically, as targeted diagnostic tools rather than always-on machinery.
Complete Example
import warnings
def fetch_data(url, timeout=None, legacy_retries=None):
if legacy_retries is not None:
warnings.warn(
"the 'legacy_retries' parameter is deprecated and ignored; "
"configure retries via a Retry object instead",
DeprecationWarning,
stacklevel=2,
)
return _do_fetch(url, timeout)
def _do_fetch(url, timeout):
return {"url": url, "status": 200}
# consumer code, opting to treat deprecations as failures during development
with warnings.catch_warnings():
warnings.simplefilter("error", category=DeprecationWarning)
try:
fetch_data("https://example.com", legacy_retries=3)
except DeprecationWarning as e:
print("caught as error during development:", e)Mental Model and Quick Reference
Think of warnings as a dial between silence and a full exception: the same warnings.warn() call can be ignored, shown once, shown always, or escalated to an error, purely through filter configuration — the code that issues the warning never changes.
warnings.warn(msg, Category, stacklevel=2) -> issue a warning
warnings.filterwarnings(action, category=...) -> configure how warnings are handled
warnings.catch_warnings() -> scope a filter change to a block
-X dev / PYTHONDEVMODE=1 -> extra runtime checks for development
faulthandler.enable() -> traceback on fatal low-level crashes
python -m trace -> line-level execution tracingKey Takeaways
warnings.warn() signals a probable problem without stopping execution, distinct from raising an exception.
Warning categories (DeprecationWarning, FutureWarning, ResourceWarning, etc.) let consumers filter by kind; custom categories can be defined by subclassing Warning.
Filters (ignore, default, error, always) control what actually happens when a warning fires, configurable via code, command line (-W), or PYTHONWARNINGS.
Turning DeprecationWarning into an error in tests is an effective way to catch deprecated API usage before it breaks in a future upgrade.
-X dev (development mode) bundles several diagnostic checks together for local development; it should not be used in production due to overhead.
faulthandler and the trace module provide lower-level diagnostics — crash tracebacks and execution tracing — beyond what normal exceptions and debugging cover.
warnings.warn("this feature is deprecated") # no category, no stacklevel warnings.warn( "this feature is deprecated, use new_feature() instead", DeprecationWarning, stacklevel=2,)
Without a category, the warning defaults to UserWarning, which misrepresents intent and makes it harder for consumers to filter deprecations specifically; without stacklevel=2, the warning points at the library's internal line instead of the caller's code.
warnings.filterwarnings("ignore") # silences every warning, everywhere, permanently with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=ResourceWarning) known_noisy_call()
A blanket, unscoped 'ignore' filter can hide genuinely important warnings elsewhere in the program; scope suppression narrowly (by category and to a specific block) instead.
Development mode's extra checks (asyncio debug mode, additional runtime assertions) carry real performance overhead and are meant for catching bugs during development, not for permanent production use.
What is the fundamental difference in intent between raising an exception and calling warnings.warn()?
An exception represents a definite error that (unless caught) stops normal execution; a warning flags a probable problem or upcoming change while allowing the program to continue running normally.
Why does warnings.warn() typically take stacklevel=2 when called from inside a library function?
stacklevel controls which frame the warning is attributed to; stacklevel=2 attributes it to the caller of the function issuing the warning (rather than the line inside the function itself), which is the actually useful location for the developer to see and fix.
Why is turning DeprecationWarning into an error a good practice in a test suite specifically, rather than in a shipped application?
In tests it surfaces use of deprecated APIs early so they can be fixed before removal breaks the code in a future dependency upgrade; in a shipped application, escalating warnings to errors risks crashing end users over conditions that were only meant to be informational for developers.
What does Python's development mode (-X dev) enable, in general terms, and why shouldn't it be used in production?
It enables a bundle of extra diagnostics: DeprecationWarning/ResourceWarning shown by default, faulthandler, asyncio debug mode, and additional runtime checks. It should not be used in production because these checks add real performance overhead and are intended for catching bugs during development.
What is the purpose of the faulthandler module, and in what situation does it help where a normal Python traceback would not appear?
faulthandler dumps a Python-level traceback on fatal low-level errors like segmentation faults or aborts (often originating in C extensions), situations where the interpreter would otherwise crash silently without producing any normal traceback.
How would you silence a specific, known ResourceWarning only within one block of code, without affecting warning behavior elsewhere in the program?
Use warnings.catch_warnings() as a context manager and call warnings.filterwarnings('ignore', category=ResourceWarning) inside it; the filter change is scoped to that block and automatically restored afterward.
Why might a library define its own custom warning category instead of using the built-in UserWarning for everything?
A custom category (subclassing Warning) lets consumers of the library filter that library's specific warnings independently from built-in categories or other libraries' warnings, giving finer-grained control.
Write a function old_api() that issues a DeprecationWarning pointing at its caller (correct stacklevel) recommending new_api() instead, then calls and returns new_api().
import warnings def new_api(): return "result" def old_api(): warnings.warn( "old_api() is deprecated; use new_api() instead", DeprecationWarning, stacklevel=2,) return new_api() stacklevel=2 ensures the warning is reported at the line in the caller's code that invoked old_api(), rather than at the warnings.warn() line itself, which is what makes the warning actionable for whoever called the deprecated function.
What gets printed when this code runs?
It prints "caught: something odd happened". Because the filter escalates UserWarning to an error, warnings.warn() raises a UserWarning exception instead of just printing a message, so 'after warn' never executes and the except block catches it instead.
Write code using warnings.catch_warnings() to suppress ResourceWarning only around a call to a function leaky_operation(), while leaving the default warning behavior intact everywhere else.
import warnings def leaky_operation(): # a known, deliberate case that would otherwise emit ResourceWarning... with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=ResourceWarning) leaky_operation() # outside the block, ResourceWarning filtering is back to whatever it was before catch_warnings() saves and restores the warnings filter state around the with-block, so the ignore filter for ResourceWarning applies only while leaky_operation() runs and does not leak into the rest of the program.
Does the warning get printed, given that ConfigWarning is a subclass of UserWarning and the filter ignores UserWarning?
No output from the warning appears; only 'done' prints. Because ConfigWarning subclasses UserWarning, it matches a filter registered for UserWarning (filters match a category or any of its subclasses), so it is silently ignored just like a plain UserWarning would be.
import warnings
warnings.filterwarnings("error", category=UserWarning)
try:
warnings.warn("something odd happened")
print("after warn")
except UserWarning as e:
print("caught:", e)class ConfigWarning(UserWarning):
pass
import warnings
warnings.filterwarnings("ignore", category=UserWarning)
warnings.warn("legacy key ignored", ConfigWarning)
print("done")