Raising Exceptions
raise is how your own code signals that something has gone wrong — and raise ... from ... lets you do it without erasing the original failure.
Exceptions are not only something you catch — they're something you produce. The raise statement lets any piece of code signal an error condition by creating and throwing an exception object, which then propagates up the call stack until something handles it or the program terminates. Knowing how to raise deliberately, re-raise correctly, and chain exceptions is core to writing code that fails clearly instead of silently.
The raise Statement
raise takes an exception instance (or a class, which Python instantiates with no arguments) and throws it. Execution of the current statement stops immediately; control jumps to the nearest enclosing except clause that matches the exception's type, or, if none matches, propagates further up the call stack.
Both branches raise a built-in ValueError with a message describing exactly what went wrong — far more useful to a caller than returning None or a sentinel.
Prefer raising the most specific built-in exception that matches the failure (ValueError, TypeError, KeyError, IndexError, FileNotFoundError, ...) before reaching for a custom exception class or the generic Exception.
def withdraw(balance: float, amount: float) -> float:
if amount <= 0:
raise ValueError(f"amount must be positive, got {amount}")
if amount > balance:
raise ValueError(f"insufficient funds: balance={balance}, amount={amount}")
return balance - amount
withdraw(100.0, -5.0) # raises ValueError immediatelyRaising with a Message and Arguments
Exception classes are ordinary classes; their __init__ typically accepts a message (and sometimes extra data) that becomes available via str(exc) and exc.args. A clear, specific message is part of writing good error-handling code — it's often the only information a maintainer sees in a traceback.
args is a tuple of the constructor's positional arguments; str() on most built-in exceptions renders it readably (KeyError wraps its single arg in repr, which is why it shows quotes).
try:
raise KeyError("config")
except KeyError as exc:
print(exc.args) # ('config',)
print(str(exc)) # "'config'"Bare raise: Re-raising the Active Exception
Inside an except block, a bare raise (no expression) re-raises the exception currently being handled, preserving its original traceback. This is the correct way to say 'do some cleanup or logging, then let the caller still see this failure' — as opposed to catching it and creating a brand-new exception, which would obscure where the problem actually started.
The caller of process() still receives the original ValueError with its full traceback, but a log entry was recorded on the way out.
A bare raise is only valid while an exception is being handled (inside an except block, or a function it calls). Used elsewhere, it raises a RuntimeError because there is no 'active exception' to re-raise.
def process(record: dict) -> None:
try:
validate(record)
except ValueError:
log.error("validation failed for %r", record)
raise # re-raise the same ValueError, traceback intactException Chaining with raise ... from ...
When you catch one exception and raise a different one in response (e.g. wrapping a low-level error in a higher-level, domain-specific one), Python automatically records the original exception as the new one's __context__ (implicit chaining). raise NewExc(...) from original_exc makes that link explicit and sets __cause__ instead — both show up in the traceback as 'The above exception was the direct cause of the following exception'.
The caller sees a ConfigError describing the high-level problem, but the traceback still shows the original FileNotFoundError as the documented cause — nothing is hidden.
Both tracebacks are printed, chained, so nothing about the original failure is lost even though a different exception type ultimately propagates.
class ConfigError(Exception):
pass
def load_config(path: str) -> dict:
try:
with open(path) as f:
return parse(f.read())
except FileNotFoundError as exc:
raise ConfigError(f"cannot load config from {path}") from excTraceback (most recent call last):
File "app.py", line 6, in load_config
with open(path) as f:
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 load_config
raise ConfigError(...) from exc
ConfigError: cannot load config from settings.iniImplicit Chaining and __context__ vs __cause__
If you raise a new exception inside an except block without using from, Python still chains it implicitly, storing the caught exception in the new exception's __context__ attribute, and the traceback prints 'During handling of the above exception, another exception occurred'. from exc additionally sets __cause__, which signals an intentional, documented relationship rather than an incidental one.
No 'from' was used, so Python sets __context__ automatically. The traceback still shows both exceptions, but frames it as 'during handling of', not 'the direct cause of'.
raise NewExc(...) from None explicitly suppresses chaining — the traceback shows only the new exception. Use it sparingly, only when the original exception is truly irrelevant noise (e.g. an internal sentinel exception used for control flow), since suppressing it discards debugging information.
try:
1 / 0
except ZeroDivisionError:
raise ValueError("bad input") # implicit chaining via __context__Preserving Context: Why It Matters
Losing the original exception when you translate it into another type is one of the most common ways debugging gets harder than it needs to be. Without from, a maintainer investigating a ConfigError has no idea it was actually caused by a missing file three layers down. Chaining costs nothing and should be the default whenever one exception is raised in response to catching another.
Both versions technically chain (Python does this automatically), but writing 'from exc' explicitly documents the relationship and protects it from accidental suppression during refactors.
# Losing context (avoid):
try:
risky_call()
except OSError:
raise RuntimeError("operation failed") # no 'from', original still chained implicitly but easy to suppress by mistake with 'from None'
# Preserving context explicitly (preferred):
try:
risky_call()
except OSError as exc:
raise RuntimeError("operation failed") from excRaising Without an Active Exception
raise always needs either an exception instance/class argument, or an active exception to re-raise bare. raise SomeError (a class, not an instance) is valid shorthand — Python calls the class with no arguments to build the instance, equivalent to raise SomeError().
Both forms are common; use the instance form whenever you have a message or extra arguments to attach.
raise ValueError # same as raise ValueError()
raise ValueError("bad") # instance with a messageCommon Patterns in Practice
Two shapes dominate real code: guard clauses that raise early on invalid input, and translation layers that catch a low-level exception and raise a higher-level, more meaningful one for callers.
A guard clause pattern: validate first, raise immediately and specifically, so the function body below can assume valid input.
def set_age(age: int) -> None:
if not isinstance(age, int):
raise TypeError(f"age must be int, got {type(age).__name__}")
if age < 0:
raise ValueError(f"age must be non-negative, got {age}")
self.age = ageassert vs raise
assert condition, message raises AssertionError if condition is falsy, but it exists for internal sanity checks and debugging, not for validating program input. Critically, assert statements are stripped out entirely when Python runs with optimizations enabled (python -O), so code relying on assert to enforce real invariants can silently stop checking anything in production.
raise always executes regardless of interpreter flags; assert is meant for catching programmer errors during development (invariants that should never be false if the code is correct), not for validating untrusted external input.
# Wrong: using assert to validate user input
def set_age(age):
assert age >= 0, "age must be non-negative" # can be skipped with -O
self.age = age
# Right: raise explicitly for conditions that must always be checked
def set_age(age):
if age < 0:
raise ValueError("age must be non-negative")
self.age = ageRaising in Library Code: Documenting Exceptions
When writing a function others will call, document which exceptions it can raise and under what conditions — typically in the docstring. This is part of a function's contract just as much as its parameters and return type, and lets callers write correct except clauses without reading the implementation.
A caller reading only the docstring now knows exactly which exception types to expect and can catch them specifically, without needing to trace through parse_config's implementation.
def parse_config(path: str) -> dict:
"""Load and parse a config file.
Raises:
FileNotFoundError: if path does not exist.
ConfigParseError: if the file exists but is not valid config syntax.
"""
...Key Takeaways
raise expr throws an exception instance (or instantiates a class) and stops normal execution at that point.
A bare raise inside an except block re-raises the currently handled exception with its original traceback intact.
raise NewExc(...) from original sets __cause__ and documents an intentional exception translation in the traceback.
Without from, Python still chains implicitly via __context__; from None explicitly suppresses chaining.
Always prefer preserving context over swallowing the original exception — it is essential for debugging.
try: do_work() except ValueError: raise RuntimeError("failed") from None try: do_work() except ValueError as exc: raise RuntimeError("failed") from exc
from None discards the original exception's traceback entirely, hiding the real root cause; chain it with from exc unless the original truly adds no information.
except ValueError: e = ValueError("new message") raise e except ValueError as exc: raise ValueError("new message") from exc
Constructing and raising a brand-new exception loses the automatic chain unless you explicitly say from exc; a bare raise alone (no new exception) would have preserved everything with less code.
def f(): raise # outside any except block def f(): raise ValueError("explicit message")
A bare raise with no active exception being handled raises RuntimeError('No active exception...'); it only works inside (or beneath) an except block.
What is the difference between raise ValueError and raise ValueError()?
None functionally — raise ValueError (a class) is shorthand; Python instantiates it with no arguments, equivalent to raise ValueError().
What does a bare raise do, and where is it valid?
It re-raises the exception currently being handled, preserving its original traceback. It is only valid inside an except block (or in code called from one) where an exception is active; otherwise it raises RuntimeError.
What is the difference between __cause__ and __context__?
__context__ is set automatically whenever a new exception is raised while handling another (implicit chaining). __cause__ is set only when you use raise ... from ..., marking the relationship as intentional and documented.
Why is raise NewError(...) from exc usually better than just raise NewError(...) inside an except block?
Both chain implicitly via __context__, but from exc sets __cause__ explicitly, documents intent, and is robust against accidental suppression; it also makes the 'direct cause' relationship clearer in the traceback header.
When is raise ... from None appropriate?
When the original exception is truly irrelevant noise to the caller (e.g. an internal control-flow exception) and showing it would only confuse debugging; use sparingly since it discards real diagnostic information.
Predict the output: what traceback message appears when you raise inside an except block without using from?
'During handling of the above exception, another exception occurred:' — indicating implicit chaining via __context__, as opposed to 'The above exception was the direct cause of the following exception:' which appears only with explicit from.
Write a function parse_int(s: str) -> int that raises a custom-message ValueError chained from the original exception if int(s) fails.
def parse_int(s: str) -> int: try: return int(s) except ValueError as exc: raise ValueError(f"could not parse {s!r} as an integer") from exc
Catches the built-in ValueError from int(), then raises a new ValueError with a clearer message, chained with from exc so the original traceback is preserved.
What exception ultimately propagates out of this code, and what does its traceback show?
TypeError('second') propagates (the bare raise re-raises it). The traceback shows the ValueError('first') block as context ('during handling of the above exception'), followed by the TypeError.
Write a guard clause function set_percentage(value: float) that raises TypeError for non-numeric input and ValueError for values outside 0-100.
def set_percentage(value: float) -> None: if not isinstance(value, (int, float)): raise TypeError(f"value must be numeric, got {type(value).__name__}") if not 0 <= value <= 100: raise ValueError(f"value must be between 0 and 100, got {value}")
Two guard clauses raise the most specific applicable built-in exception (TypeError for wrong type, ValueError for wrong-but-typed value) before any further logic runs.
try:
raise ValueError("first")
except ValueError:
try:
raise TypeError("second")
except TypeError:
raise