Raising Exceptions
Catching exceptions is only half of Python’s error-handling story. The other half is deciding when your own code should signal that something is wrong, using the raise keyword. Raising the right exception, with a clear message, at the point where invalid data or an invalid state is first detected, is one of the most effective things you can do for the people (including future you) who will debug the program later.
The `raise` Keyword
raise takes an exception instance (or, less commonly, a bare exception class, which Python instantiates with no arguments) and immediately stops normal execution, unwinding the stack until something catches it.
def set_age(age):
if age < 0:
raise ValueError(f"age cannot be negative, got {age}")
return age
set_age(-5)Traceback (most recent call last):
File "example.py", line 6, in <module>
set_age(-5)
File "example.py", line 3, in set_age
raise ValueError(f"age cannot be negative, got {age}")
ValueError: age cannot be negative, got -5Writing a Useful Message
The string passed to an exception’s constructor becomes its message, and it is the very first thing a reader sees in a traceback. A message like "invalid input" forces the reader to go digging; a message that includes the offending value and what was expected saves that trip entirely.
def get_discount(code, catalog):
if code not in catalog:
raise KeyError(f"unknown discount code {code!r}; valid codes: {sorted(catalog)}")
return catalog[code]Re-raising a Caught Exception
Sometimes you want to inspect or log an exception without actually handling it — the caller still needs to see it. A bare raise with no arguments, used inside an except block, re-raises the exception that is currently being handled, preserving its original traceback.
def load_settings(path):
try:
return parse(open(path).read())
except ValueError:
log.error("settings file at %s is malformed", path)
raise # re-raise the same ValueError, traceback intactException Chaining with `raise ... from ...`
When you catch one exception and raise a different, more meaningful one in response, raise NewException(...) from original links the two together. Python prints both in the traceback, so nothing about the root cause is lost even though the caller only needs to handle the new, higher-level exception type.
class ConfigError(Exception):
pass
def load_config(path):
try:
return parse(open(path).read())
except FileNotFoundError as e:
raise ConfigError(f"configuration file missing: {path}") from eTraceback (most recent call last):
File "app.py", line 6, in load_config
return parse(open(path).read())
FileNotFoundError: [Errno 2] No such file or directory: 'settings.ini'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "app.py", line 9, in <module>
load_config("settings.ini")
File "app.py", line 8, in load_config
raise ConfigError(f"configuration file missing: {path}") from e
ConfigError: configuration file missing: settings.ini`assert` for Internal Invariants
assert checks a condition and raises AssertionError if it is false, optionally with a message. It exists to check things that should be impossible if your own code is correct — internal invariants and programmer assumptions — not to validate data coming from outside the program.
def average(numbers):
assert len(numbers) > 0, "average() called with an empty list"
return sum(numbers) / len(numbers)raise SomeException("message")— signal a problem with a clear, specific message.raise(no argument) insideexcept— re-raise the exception currently being handled, unchanged.raise NewException(...) from original— chain a new exception to the one that caused it.assert condition, "message"— check internal invariants only; stripped under-O, never for real validation.