PYTHON / ERRORS AND EXCEPTIONS
Raising and re-raising exceptions
Raise exceptions deliberately with useful messages, and use a bare raise to pass a partly handled exception back to the caller.
What you will learn
- Raise built-in exception types with a message that names the offending value
- Know that raise ValueError and raise ValueError('msg') both produce an instance
- Use bare raise inside an except block to re-raise the exception being handled
- Recognise RuntimeError: No active exception to reraise and what causes it
Understanding Raising and re-raising exceptions
The raise statement takes either an exception instance or an exception class. If you give it a class, Python calls it with no arguments for you, so raise ValueError and raise ValueError() are the same thing and both produce an empty message. That is why raise ValueError(f"age must be non-negative, got {age}") is worth the extra typing: the argument becomes exc.args and therefore str(exc), which is what the caller and the traceback will show. Anything that is not a BaseException subclass or instance is rejected up front with TypeError, so raise "bad input" never works.
Inside an except block you can write raise with no argument at all. That re-raises the exception currently being handled, the same object, with the traceback it has already accumulated. This is what makes partial handling possible: a function can notice the failure, record it, count it, close something, print a diagnostic, and still refuse to decide what the failure means, leaving that to whoever called it. Silently swallowing an exception is a policy decision, and a low-level helper is usually the wrong place to make it.
The bare form only works while an exception is active. Once the except block ends, Python clears the current exception, so a bare raise after the handler, or anywhere at module level, fails with RuntimeError: No active exception to reraise. Writing raise exc instead, where exc is the name bound by except ... as exc, also propagates the same object, but each raise of an already-raised object appends the current line to its traceback, so you get an extra frame pointing at the handler. Prefer the bare form when you mean "keep going as if I had not caught this".
def set_age(age):
if age < 0:
raise ValueError(f"age must be non-negative, got {age}")
return age
def parse_age(text):
try:
return set_age(int(text))
except ValueError:
print("parse_age: rejecting", repr(text))
raise
for value in ["31", "-4", "abc"]:
try:
print("accepted", parse_age(value))
except ValueError as exc:
print("caller saw:", type(exc).__name__, exc)
A bare raise inside an except block re-raises the exception being handled, with its original traceback, so a function can react to a failure without deciding its outcome.
Worked examples
Class, instance, and nothing at all
Shows what raise does with an exception class, with a constructed instance, and with no argument when no exception is active.
def show(spec):
try:
raise spec
except Exception as exc:
print(type(exc).__name__, "| args =", exc.args, "| str =", repr(str(exc)))
show(ValueError)
show(ValueError("bad rate", 0.5))
try:
raise
except RuntimeError as exc:
print("bare raise with nothing active ->", exc)
Example explained
Line 1raise ValueError passes a class, so Python instantiates it with no arguments: args is empty and str(exc) is the empty string.
Line 2Two constructor arguments both land in args, and str() of a multi-argument exception is the repr of the whole tuple, which is rarely what you want in a log line.
Line 3The final bare raise runs outside any handler, so there is nothing to re-raise and Python raises RuntimeError instead.
Retry, then re-raise the real failure
Re-raises the caught exception only after the last attempt, so the caller sees the actual error rather than a substitute.
def unstable(results):
it = iter(results)
def call():
outcome = next(it)
if isinstance(outcome, Exception):
raise outcome
return outcome
return call
def retry(call, times):
for attempt in range(1, times + 1):
try:
return call()
except ConnectionError as exc:
print(f"attempt {attempt} failed: {exc}")
if attempt == times:
raise
print(retry(unstable([ConnectionError("reset"), ConnectionError("timeout"), "payload"]), 3))
try:
retry(unstable([ConnectionError("reset"), ConnectionError("reset")]), 2)
except ConnectionError as exc:
print("out of attempts:", exc)
Example explained
Line 1raise outcome shows that exceptions are ordinary objects: they can be stored in a list and raised later.
Line 2The handler prints every failure but only re-raises on the last attempt, so transient errors are absorbed and permanent ones are not.
Line 3Because the bare raise re-raises the caught ConnectionError, the caller gets the genuine message 'reset' instead of an invented one.
Line 4If the loop ended without raising, retry would return None and the caller could not tell success from failure.
Validating before work happens
Raising early keeps invalid state from reaching the rest of the function.
def split_bill(total, people):
if people <= 0:
raise ValueError(f"people must be positive, got {people}")
if not isinstance(total, (int, float)):
raise TypeError(f"total must be a number, got {type(total).__name__}")
return round(total / people, 2)
print(split_bill(42, 4))
for bad in [(42, 0), ("42", 4)]:
try:
split_bill(*bad)
except (ValueError, TypeError) as exc:
print(f"{type(exc).__name__}: {exc}")
Example explained
Line 1raise stops the function immediately, so the division on the last line only ever runs with checked inputs.
Line 2ValueError signals a value of the right type that is unusable; TypeError signals the wrong kind of object entirely.
Line 3Each message includes the offending value, which is what makes the exception debuggable without a debugger.
Important notes
raise exc and bare raise both propagate the same object in Python 3, but re-raising an object adds the current line to its traceback, so bare raise gives the cleaner report.
raise only accepts BaseException subclasses or instances; raise "disk full" gives TypeError: exceptions must derive from BaseException.
Common mistakes
Writing raise ValueError instead of raise ValueError("..."), which produces an exception with an empty message, so the traceback shows only the type name.
Catching an exception, printing it, and forgetting the raise: the function then returns None and the caller happily treats a failure as a valid result.
Putting a bare raise after the except block, or in a normal branch, which fails with RuntimeError: No active exception to reraise instead of the intended error.
Try it yourself
Change, predict, then run
Write check_percent(value) that raises ValueError with the offending value in the message unless 0 <= value <= 100, then wrap it in a function that catches ValueError, increments a rejection counter, prints it, and re-raises with a bare raise. Call the wrapper with 50, 150, and -1 inside a try/except and confirm the counter reaches 2 while the caller still sees each ValueError.
Open the Python workspaceCheck your understanding
A helper catches ValueError as exc, prints a diagnostic, and then executes raise exc instead of a bare raise. What actually happens?
- The same exception keeps propagating, and its traceback now also includes the handler's raise line
- The original traceback is discarded, so the caller only sees the handler frame
- The re-raise fails with RuntimeError because the exception was already caught
- Python wraps it in a new exception whose __cause__ is the original one
Show answer
In Python 3 the traceback travels with the exception object in its __traceback__ attribute, so re-raising the same object preserves the original frames and appends the frame where the new raise occurred. Option 2 describes Python 2 behaviour, where the traceback was not attached to the object and had to be passed explicitly; option 4 requires an explicit raise ... from.