5.26. Raising errors¶
A function can signal a problem to its caller by raising an
exception. The keyword is raise:
def square_root(x):
if x < 0:
raise ValueError("square_root expects a non-negative number")
return x ** 0.5
Calling square_root(-1) stops at the raise line, jumps
out of the function, and looks for a matching except in the
caller. If no caller catches it, the script ends with a
traceback.
5.26.1. Why raise instead of returning a sentinel¶
Two ways to report “bad input”:
# signal with a sentinel
def square_root_or_none(x):
if x < 0:
return None
return x ** 0.5
# raise an exception
def square_root(x):
if x < 0:
raise ValueError("...")
return x ** 0.5
The exception form is usually better:
The caller has to deliberately handle the error case – either with a
try, or by letting the exception propagate. Sentinels are easy to forget and easy to mistake for a normal result.The error message travels with the exception; the sentinel approach has to attach the diagnostic somewhere else.
The default behaviour on an unhandled exception is a loud crash with a traceback that points at the offending call. Silent
Nonereturns become subtle bugs later.
Reach for sentinels only when “not found” is a routine,
non-exceptional outcome – dict.get() returns None
on a missing key precisely because lookups are expected to
sometimes miss.
5.26.2. Custom exception classes¶
To raise a problem the caller might want to distinguish from
built-in errors, define a subclass of Exception:
class ConfigError(Exception):
pass
def load_config(path):
try:
f = open(path)
except OSError as e:
raise ConfigError("missing config file: " + path)
try:
load_config("settings.json")
except ConfigError as e:
print("startup failed:", e)
The empty class body is fine – the name itself is what
matters, because callers catch by class. Group related errors
under a common base if a caller might want to catch the whole
family in one block.
5.26.2.1. Re-raising¶
A bare raise inside an except block re-raises the
current exception so it propagates to the next handler:
try:
do_work()
except Exception as e:
log(e)
raise # let it keep going
This is the right shape when a function wants to observe an error (log it, count it, undo a partial change) without actually handling it.
5.26.3. When to catch and when to propagate¶
A useful rule of thumb:
Catch an exception at the level that can meaningfully recover – substitute a default, retry, skip the bad input.
Let it propagate when there is nothing useful to do except crash, or when the layer above is the one that knows how to recover.
A function in the middle of a call stack that swallows every error and returns silently makes failures untraceable. Prefer to let exceptions travel until they reach code that genuinely has a plan.