Python Basics: Preserving Causes with Exception Chaining
A low-level failure may need a clearer explanation at the boundary of your application. Exception chaining lets you raise a meaningful new error while retaining the original cause for debugging. These examples handle expected failures so every script exits successfully.
These three standalone examples were tested on Linux with Python 3.12.14. They require no third-party packages. Assertions check the demonstrated results; run the commands normally, without Python’s -O option.
Prepare a scratch directory
You need Bash, a text editor, Python 3 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file as UTF-8. The setup stops the shell if creating or entering the scratch directory fails.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Translate an input failure without losing its cause
Save this as parse_quantity.py:
class QuantityError(ValueError):
pass
def parse_quantity(text):
try:
return int(text)
except ValueError as cause:
raise QuantityError("Quantity must be a whole number") from cause
assert parse_quantity("12") == 12
try:
parse_quantity("twelve")
except QuantityError as error:
assert isinstance(error.__cause__, ValueError)
print(str(error))
print(f"Cause: {type(error.__cause__).__name__}")
else:
raise AssertionError("Expected QuantityError")Run it:
python3 parse_quantity.pyOutput:
Quantity must be a whole number
Cause: ValueErrorThe caller handles QuantityError while a debugger can inspect its cause. The narrow except clause translates a known conversion failure. Catching every Exception here could hide unrelated programming mistakes behind a misleading quantity message.
2. Re-raise the same exception when translation is unnecessary
Save this as reraise_failure.py:
def parse_count(text):
try:
return int(text)
except ValueError:
print("Count conversion failed")
raise
try:
parse_count("many")
except ValueError as error:
assert error.__cause__ is None
print(f"Caller received: {type(error).__name__}")
else:
raise AssertionError("Expected ValueError")Run it:
python3 reraise_failure.pyOutput:
Count conversion failed
Caller received: ValueErrorA bare raise inside an exception handler re-raises the active exception. Use it when you want to observe a failure and let the original exception continue to the caller. No new application-specific error is needed in this example.
3. Suppress displayed context deliberately
Save this as hide_internal_context.py:
def require_setting(settings, name):
try:
return settings[name]
except KeyError:
raise ValueError(f"Required setting is missing: {name}") from None
try:
require_setting({}, "theme")
except ValueError as error:
assert error.__cause__ is None
assert isinstance(error.__context__, KeyError)
assert error.__suppress_context__ is True
print(str(error))
print(f"Context display suppressed: {error.__suppress_context__}")
print(f"Stored context: {type(error.__context__).__name__}")
else:
raise AssertionError("Expected ValueError")Run it:
python3 hide_internal_context.pyOutput:
Required setting is missing: theme
Context display suppressed: True
Stored context: KeyErrorfrom None suppresses the original context in the normal traceback display; it does not erase the stored context. This can simplify a public-facing error, but it is not a way to remove secrets from memory or logs. Prefer explicit chaining when the original failure is useful to someone diagnosing the problem.
When to use this
These scripts print stable summaries rather than interpreter-dependent traceback formatting. In a real uncaught chained failure, Python displays the cause and the new exception together. The official exception-chaining tutorial explains raise … from and context suppression.
Clean up
In the same terminal, remove only the three example files, return to your original directory, and remove the empty scratch directory:
rm -- parse_quantity.py reraise_failure.py hide_internal_context.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.