Skip to content

Exceptions

An exception is a signal that something went wrong during execution. When Python raises an exception, it unwinds the call stack until it finds a matching except clause or terminates the program.

Exceptions are objects — instances of classes that inherit from BaseException. The ones you will work with in normal code almost always inherit from Exception.

Python’s try statement has four optional clauses:

try:
result = risky_operation()
except SomeError as e:
# runs only if SomeError (or a subclass) was raised
handle(e)
else:
# runs only if NO exception was raised in try
use(result)
finally:
# ALWAYS runs — exception or not
cleanup()

The else clause is a useful Python feature: it lets you distinguish “the operation succeeded” from “we are in cleanup.” Code in else only runs on the happy path.

Always catch the most specific exception type you can. Catching Exception is a last resort; catching bare except: silently swallows KeyboardInterrupt and SystemExit as well.

try:
n = int(user_input)
except ValueError:
print("Not a valid integer")
except TypeError:
print("Expected a string, not None")

To catch multiple types in one clause, use a tuple:

except (ValueError, TypeError) as e:
print(f"Input error: {e}")

Use raise to signal an error explicitly:

def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("divisor cannot be zero")
return a / b

Re-raise the current exception inside an except block with a bare raise:

except ValueError:
log_error()
raise # re-raises the same exception unchanged

When you catch one exception and raise another, use raise NewError(...) from original to preserve the original as the cause. This keeps the full traceback in the error chain.

try:
value = int(raw_string)
except ValueError as original:
raise RuntimeError("Failed to parse config") from original

The __cause__ attribute on the new exception holds the original. Python displays both tracebacks automatically.

To explicitly discard the cause (suppress the chained traceback), use raise ... from None.

def safe_divide(a: float, b: float) -> float:
"""Divide a by b, raising ValueError if b is zero."""
if b == 0:
raise ValueError("divisor cannot be zero")
return a / b
# --- try / except / else / finally ---
print("=== basic try/except/else/finally ===")
for b in [2.0, 0]:
try:
result = safe_divide(10.0, b)
except ValueError as e:
print(f" ValueError: {e}")
else:
print(f" Result: {result}")
finally:
print(" finally ran")
# --- catching specific types ---
print("\n=== specific exception types ===")
for val in ["42", "abc", None]:
try:
n = int(val)
print(f" parsed {val!r} -> {n}")
except (TypeError, ValueError) as e:
print(f" {type(e).__name__} for {val!r}: {e}")
# --- exception chaining ---
print("\n=== exception chaining ===")
try:
try:
raw = "not-a-number"
value = int(raw)
except ValueError as original:
raise RuntimeError("Failed to parse config value") from original
except RuntimeError as e:
print(f" RuntimeError: {e}")
print(f" __cause__: {e.__cause__}")
When does the `else` block in a try statement run?
What does `raise RuntimeError('msg') from original` do?
Which clause in a try statement ALWAYS runs, even if an exception is raised?
What is the problem with bare `except:` (no exception type)?