PYTHON / ERRORS AND EXCEPTIONS
Warnings and logging failures
Record recoverable failures with logging.exception and flag advisory problems with warnings.warn, instead of printing or silently swallowing them.
What you will learn
- Log a caught exception with its traceback using log.exception or exc_info=True
- Choose between raising, warning, and logging based on who must act on the problem
- Control which records appear with logger levels and basicConfig instead of print
- Emit and capture warnings, including routing them into logging with captureWarnings
Understanding Warnings and logging failures
Catching an exception hides the traceback, because the interpreter only prints one for exceptions that reach the top of the program. If you handle a ValueError and continue, the evidence of what went wrong exists only inside the except block, and a bare print("error") discards the line numbers, the exception type, and the chain of causes. The logging module keeps that evidence: a record carries a severity, a logger name, and optionally the formatted traceback, and the destination is decided by configuration rather than by the code that failed.
log.exception(msg) is exactly log.error(msg, exc_info=True), and exc_info=True means "attach whatever sys.exc_info() currently holds". That is why it only produces a traceback while an exception is being handled; call it elsewhere and the record ends with the useless line NoneType: None. Pass message arguments separately, as log.error("bad port %r", raw), not as an f-string: the record stores the pattern and the values, and formatting is skipped entirely when the level is disabled.
Warnings cover a different case, where nothing has failed yet but something should change: a deprecated function, a suspicious argument, a resource left open. warnings.warn returns normally, so control flow is untouched, and the warnings filters, not your code, decide whether the text is shown, shown once, or turned into an error. A useful split is: raise when the caller must decide what happens next, warn when the caller should change their code eventually, and log when the program is going to carry on and you need a durable record of why.
import logging
import sys
logging.basicConfig(
level=logging.INFO,
stream=sys.stdout,
format="%(levelname)s %(name)s: %(message)s",
)
log = logging.getLogger("config")
def read_port(raw):
try:
return int(raw)
except ValueError:
log.exception("bad port %r; falling back to 8080", raw)
return 8080
print(read_port("9090"))
print(read_port("80a80"))A failure you recover from still needs a record, and logging preserves the traceback that catching the exception threw away.
Worked examples
A warning does not stop the function
Shows that warnings.warn returns normally and that warnings can be captured as objects instead of printed.
import warnings
def new_api(x):
return x * 2
def old_api(x):
warnings.warn("old_api is deprecated; use new_api", DeprecationWarning, stacklevel=2)
return new_api(x)
with warnings.catch_warnings(record=True) as seen:
warnings.simplefilter("always")
print(old_api(3))
print(old_api(4))
print([str(w.message) for w in seen])
print([w.category.__name__ for w in seen])Example explained
Line 1warnings.warn(...) does not raise, so the call to new_api(x) on the next line still runs and returns 6.
Line 2stacklevel=2 makes the reported source location the line that called old_api, which is what the person who must fix the code cares about.
Line 3catch_warnings(record=True) collects each warning as an object with .message and .category instead of writing text to stderr.
Line 4simplefilter("always") is needed to see both calls; the default filters would deduplicate the second one by source location.
Inspecting the exc_info attached to a record
Demonstrates that exc_info=True stores the live exception triple on the log record.
import logging
records = []
class Collect(logging.Handler):
def emit(self, record):
records.append(record)
log = logging.getLogger("job")
log.setLevel(logging.DEBUG)
log.addHandler(Collect())
def safe_div(n):
try:
return 100 // n
except ZeroDivisionError:
log.error("safe_div(%d) failed", n, exc_info=True)
return None
print(safe_div(4), safe_div(0))
for r in records:
print(r.levelname, r.getMessage(), r.exc_info[0].__name__)Example explained
Line 1log.setLevel(logging.DEBUG) is required because a logger's default effective level comes from the root logger, which is WARNING.
Line 2record.getMessage() applies the %d formatting only now, which is why the argument n is passed separately rather than interpolated early.
Line 3record.exc_info is the (type, value, traceback) triple captured by exc_info=True, so the handler can format the traceback later.
Line 4safe_div(4) produces no record at all, since nothing was raised and the except block never ran.
Sending warnings through logging
Shows logging.captureWarnings redirecting warnings.warn output into the py.warnings logger.
import logging
import warnings
seen = []
class Collect(logging.Handler):
def emit(self, record):
seen.append((record.name, record.levelname, record.getMessage()))
logging.getLogger("py.warnings").addHandler(Collect())
logging.captureWarnings(True)
warnings.simplefilter("always")
warnings.warn("cache directory is unwritable", RuntimeWarning)
print(seen[0][0], seen[0][1])
print("cache directory is unwritable" in seen[0][2])
print("RuntimeWarning" in seen[0][2])Example explained
Line 1captureWarnings(True) replaces warnings.showwarning, so warning text stops going to stderr and becomes a log record instead.
Line 2Every captured warning is logged on the fixed logger name py.warnings at level WARNING, whatever the warning category was.
Line 3The message is the fully formatted warning text, so the category name and source location end up inside the message string rather than in separate fields.
Line 4The handler is attached to py.warnings directly, which is why no basicConfig call is needed for the record to be delivered.
Important notes
Traceback text in log output contains real file names and line numbers, so the File "...", line N lines you see will match your own file, not the example.
Default filters show a given warning only once per source location, so a warning inside a loop appears once; that is filter behaviour, not the code being skipped.
Common mistakes
Writing except Exception: print("something went wrong") — the traceback, exception type, and cause are gone, so the log tells you a failure happened but not where or why.
Calling log.info(...) or log.debug(...) with no configuration and concluding logging is broken; the root logger's level is WARNING, so those records are discarded before any handler runs.
Calling log.exception outside an except block, which appends the literal text NoneType: None because sys.exc_info() holds no exception at that moment.
Using warnings.warn for a real failure the caller must handle; warn returns normally, DeprecationWarning is ignored by default in imported modules, so the problem silently proceeds.
Try it yourself
Change, predict, then run
Write a function parse_ages(values) that converts a list of strings to integers, skips any value that raises ValueError while logging it with log.exception, and calls warnings.warn once if more than half the values were skipped. Run it on ["31", "x", "y", "7"] and confirm both the traceback and the warning appear.
Open the Python workspaceCheck your understanding
A function catches ValueError and calls log.info("bad row", exc_info=True). The script imports logging but never configures it, and nothing at all appears when it runs. What explains this?
- The effective level of the logger is WARNING by default, so the INFO record is discarded before any handler is consulted
- exc_info=True is only honoured by log.exception, so the whole call is ignored
- Catching the exception clears sys.exc_info(), so logging drops records with empty exception information
- Logging writes to stderr, which is discarded unless the script redirects it
Show answer
The logger performs a level check first: with no configuration the effective level comes from the root logger, which is WARNING, so an INFO record never reaches a handler. Switching to log.exception would work, but not because of exc_info — exc_info=True is honoured by every logging method; log.exception simply logs at ERROR, which passes the level check.