Exception Handling in Python
Exception Handling in Python allows developers to manage errors gracefully, ensuring programs can recover from unexpected issues and continue execution smoothly.
Exception handling in Python is a comprehensive system for representing exceptional conditions as objects, raising and propagating them through execution contexts, recording traceback information, selecting appropriate handlers, guaranteeing cleanup, preserving causal relationships, grouping multiple simultaneous failures, and defining application-specific exception types. This system enables robust error management and control flow beyond ordinary return values.
Foundations of Exception Handling in Python
An exception object represents an error or special condition. It is created (instantiated) and then raised, signaling an exceptional situation. The exception propagates through the active execution contexts (the call stack) until a matching handler is found. Along this path, a traceback is attached to the exception object, recording the sequence of frames involved.
When a handler matches the exception type, it executes, optionally binding the exception to a variable for inspection. If no handler matches, the exception continues propagating upward. After handling, the program may recover or continue propagation if the exception is re-raised.
Cleanup is guaranteed by finally blocks, which run regardless of whether an exception occurred, ensuring resource release or other necessary finalization.
Exceptions represent both ordinary application failures (like file not found or invalid input) and special control-oriented conditions (like stopping iteration). Correct handling depends on the exception's type, the abstraction boundary crossed, whether the failure is recoverable, and if the program can meaningfully continue.
| Aspect | Responsibility |
|---|---|
| Exception Object | Encapsulates failure or control condition data |
| Built-in Hierarchy | Organizes exception types for matching and specialization |
| Raising | Instantiates and raises exceptions to signal errors |
| Tracebacks | Records call stack frames where exception propagated |
| Handlers | Match exceptions by type and execute recovery code |
finally | Executes cleanup code regardless of exception occurrence |
| Chaining | Links related exceptions explicitly or implicitly |
| Exception Groups | Represent multiple simultaneous exceptions as a single object |
| Custom Exceptions | Define application-specific failure types and diagnostic data |
Python Exception Objects
Exception objects are instances of classes derived from the root BaseException. They have a type indicating the exception kind, an args attribute holding constructor arguments, and a human-readable string representation. Exception-specific attributes provide further diagnostics or data.
Exception objects also carry metadata to support propagation and diagnostics:
__traceback__: The traceback object capturing the call stack at the point of raising.__context__: The previous active exception automatically chained during implicit exception propagation.__cause__: An explicitly set cause when chaining exceptions usingraise ... from ....__suppress_context__: A boolean flag indicating whether to suppress display of the implicit context.__notes__: A list of supplementary diagnostic strings attached viaadd_note().
The method BaseException.add_note attaches additional diagnostic messages to an exception without altering its type, message, cause, context, or traceback. These notes appear in standard exception output, enhancing diagnostics without changing primary exception data.
Example:
def example():
exc = ValueError("Invalid value", 42)
print(f"Type: {type(exc)}")
print(f"Args: {exc.args}")
exc.add_note("This value failed validation in example()")
try:
raise exc
except ValueError as e:
print(f"Caught exception: {e}")
print(f"Traceback: {e.__traceback__}")
print(f"Notes: {e.__notes__}")
example()
Python Built-in Exception Hierarchy
BaseException is the root of Python’s built-in exception hierarchy. It is the ancestor of all exceptions, including system-exiting exceptions like SystemExit, KeyboardInterrupt, and GeneratorExit, which are direct subclasses. These exceptions typically indicate conditions that should not be caught by general application code.
Exception is the principal base class for ordinary application-level exceptions. Most user-defined and built-in error exceptions inherit from Exception. Application-level handlers normally catch subclasses of Exception to avoid intercepting system-exiting exceptions.
The following table summarizes key branches of the built-in hierarchy and representative exceptions:
| Exception Type | Description and Examples |
|---|---|
| BaseException | Root of all exceptions. |
| ├─ SystemExit | Raised by sys.exit(), terminates the interpreter. |
| ├─ KeyboardInterrupt | Raised on user interrupt (Ctrl+C). |
| ├─ GeneratorExit | Raised when a generator or coroutine is closed. |
| └─ Exception | Base for ordinary exceptions. |
| ├─ ArithmeticError | Base for arithmetic errors. Subclasses include ZeroDivisionError, OverflowError, FloatingPointError. |
| ├─ LookupError | Base for lookup errors such as IndexError, KeyError. |
| ├─ ImportError | Import-related errors including ModuleNotFoundError. |
| ├─ NameError | Name-related errors such as UnboundLocalError. |
| ├─ OSError | Operating system-related errors, including FileNotFoundError, PermissionError. |
| ├─ RuntimeError | Generic runtime errors. |
| ├─ SyntaxError | Syntax errors, including IndentationError and TabError. |
| ├─ Warning | Base for warning categories like DeprecationWarning. |
| └─ ExceptionGroup | Represents multiple exceptions raised concurrently. Inherits from Exception. |
| BaseExceptionGroup | Represents multiple exceptions (any BaseException) raised concurrently; parent of ExceptionGroup. |
Example handler demonstrating subclass matching and ordering:
try:
1 / 0
except ZeroDivisionError:
print("Caught division by zero")
except ArithmeticError:
print("Caught arithmetic error")
except Exception:
print("Caught other exception")
The above matches ZeroDivisionError first, demonstrating narrow-before-broad ordering. A BaseException descendant like KeyboardInterrupt is not caught by except Exception.
Raising Exceptions in Python
The raise statement signals an exceptional condition by raising an exception object.
raisefollowed by an exception instance raises that instance directly.raisefollowed by an exception class automatically instantiates it with no arguments and raises the new instance.- A bare
raisere-raises the currently active exception within an exception handler. - Using a bare
raiseoutside an active exception handler raises aRuntimeError. raise new_exception from causesets the explicit cause (__cause__) for exception chaining.raise new_exception from Nonesuppresses implicit context display while preserving exception propagation.
Examples:
# Raising an instance
raise ValueError("Invalid input")
# Raising a class (auto-instantiates)
raise KeyError
# Re-raising active exception
try:
raise IndexError("Out of range")
except IndexError:
raise # Re-raises IndexError
# Explicit chaining
try:
int("abc")
except ValueError as e:
raise RuntimeError("Conversion failed") from e
# Suppressing context display
try:
1 / 0
except ZeroDivisionError:
raise RuntimeError("Failed") from None
Python Exception Tracebacks
A traceback records the runtime call stack at the point an exception propagates. It is linked to the exception object via the __traceback__ attribute. Each traceback node corresponds to a frame of execution and includes source location information such as filename, line number, and function name.
Tracebacks reflect the dynamic propagation path through calls, not static lexical nesting or root cause proof.
The method with_traceback(tb) attaches a traceback explicitly but only to the necessary depth to show the traceback.
Example:
def f():
g()
def g():
h()
def h():
raise ValueError("Error in h")
try:
f()
except ValueError as e:
tb = e.__traceback__
while tb:
frame = tb.tb_frame
lineno = tb.tb_lineno
print(f"File {frame.f_code.co_filename}, line {lineno}, in {frame.f_code.co_name}")
tb = tb.tb_next
Output shows the call stack from h through g to f.
Python Exception Handlers
The try statement with except clauses selects handlers in source order. Each handler specifies one or more exception classes; matching is based on inheritance (an exception matches a handler if its type is a subclass of any specified).
If no handler matches, the exception propagates upward. When a handler matches, the exception can be bound to a variable using as. After execution, the exception binding is cleaned up.
While handling an exception, sys.exception() returns the current exception object (in Python 3.11+).
Handlers may specify multiple exception types as a tuple, e.g., except (TypeError, ValueError) as e:. Python 3.14 allows omitting parentheses when no as target is used, e.g., except TypeError, ValueError:.
The else clause runs after the try block completes successfully (no exception, no return, break, or continue). Exceptions raised in else propagate as new exceptions.
Examples:
import sys
try:
x = int("not a number")
except ValueError as e:
print(f"Caught ValueError: {e}")
print(f"Current exception: {sys.exception()}")
except (TypeError, KeyError):
print("Caught TypeError or KeyError")
else:
print("Conversion succeeded")
Narrower exceptions should be handled before broader ones:
try:
raise KeyError("missing")
except KeyError:
print("Caught KeyError")
except Exception:
print("Caught Exception")
Unmatched exceptions propagate:
try:
raise KeyboardInterrupt()
except Exception:
print("Caught Exception")
# KeyboardInterrupt not caught here
Cleanup with finally in Python
The finally clause executes when leaving the associated try block, regardless of whether the block completes normally, raises an exception, or exits via return, break, or continue.
If an exception is active when entering finally, it is preserved and re-raised after finally executes, unless replaced by a new exception raised within finally.
If finally executes a return, break, or continue statement, it masks any pending exception or previous return value, effectively overriding it. Python 3.14 issues a SyntaxWarning for control flow statements exiting finally blocks because they can lead to hard-to-debug issues.
Examples:
# Cleanup after normal completion
try:
print("Try block")
finally:
print("Cleanup")
# Cleanup during exception
try:
raise ValueError("Error")
finally:
print("Cleanup during exception")
# New exception from finally masks old one
try:
raise ValueError("Original")
finally:
raise RuntimeError("New error")
# Discouraged return from finally masks exceptions/returns
def f():
try:
raise ValueError("Error")
finally:
return "Suppressed"
print(f())
Exception Chaining in Python
Exceptions can be chained to preserve causal relationships:
- Implicit chaining occurs automatically when an exception is raised during handling of another. The new exception's
__context__points to the original. - Explicit chaining uses
raise ... from ...to set__cause__explicitly, indicating direct causation. - Setting
from Nonesuppresses the display of implicit context (__context__) but does not delete it;__suppress_context__is set toTrueto suppress printing.
Examples:
# Implicit chaining
try:
1 / 0
except ZeroDivisionError:
raise ValueError("Conversion failed")
# Explicit chaining
try:
int("abc")
except ValueError as e:
raise RuntimeError("Failed") from e
# Suppressing context display
try:
1 / 0
except ZeroDivisionError:
raise RuntimeError("Failed") from None
# Inspect chain attributes
try:
1 / 0
except ZeroDivisionError as e:
print(f"Context before: {e.__context__}")
print(f"Cause before: {e.__cause__}")
print(f"Suppress context: {e.__suppress_context__}")
Python Exception Groups
Python Exception Group Objects
BaseExceptionGroup and ExceptionGroup represent multiple simultaneous exceptions bundled into a single exception object.
BaseExceptionGroupcan contain anyBaseExceptioninstances (including system-exiting exceptions).ExceptionGroupcontains only ordinaryExceptioninstances.
These groups have a message string and a sequence of contained exceptions, which may themselves be groups, allowing nested structures.
Operations:
subgroup: Extracts a subgroup of contained exceptions matching a predicate.split: Divides contained exceptions into matching and non-matching groups.derive: Creates a new group with a modified message or subset of exceptions, preserving structure.
Handling Exception Groups with except* in Python
The except* syntax allows handling subgroups of exceptions within an exception group:
- Handlers match subgroups of exceptions by type.
- Matching and non-matching portions are processed sequentially.
- Unhandled or newly raised exceptions recombine and propagate.
- A matching non-group exception is automatically wrapped in an exception group.
- Restrictions:
exceptandexcept*cannot coexist in the sametry.- A matching type is mandatory in
except*. - Matching
BaseExceptionGroupsubclasses directly is invalid. return,break, andcontinueare disallowed inexcept*handlers.
Example:
def raise_multiple():
raise ExceptionGroup("Multiple errors", [
ValueError("Invalid value"),
KeyError("Missing key"),
RuntimeError("Runtime problem"),
])
try:
raise_multiple()
except* ValueError as e:
print(f"Caught subgroup: {e}")
except* KeyError as e:
print(f"Caught subgroup: {e}")
# RuntimeError subgroup propagates further
Custom Exceptions in Python
Custom exceptions should derive from Exception or an appropriate built-in subclass designed for ordinary application failures. Using a coherent application-specific base class allows callers to catch the entire family when needed.
Names should communicate the failure condition clearly and concisely.
Structured diagnostic attributes should be added only if they provide meaningful machine-readable context beyond the message string.
Example hierarchy:
class AppError(Exception):
"""Base class for application errors"""
class DataError(AppError):
def __init__(self, message, field):
super().__init__(message)
self.field = field
class NetworkError(AppError):
def __init__(self, message, endpoint):
super().__init__(message)
self.endpoint = endpoint
def do_something():
try:
raise DataError("Invalid data", field="age")
except DataError as e:
print(f"Data error in field {e.field}: {e}")
except AppError:
print("General app error")
Custom exceptions should remain simple, avoid unnecessary overriding of exception machinery, and avoid multiple inheritance from built-in exceptions due to implementation conflicts. They should represent genuinely exceptional conditions rather than routine alternative results.
A disciplined exception-handling approach involves:
- Catching only failures that can be meaningfully handled or translated at the current abstraction boundary.
- Preserving diagnostic context when translating exceptions.
- Performing required cleanup independently of recovery.
- Allowing unknown or unrecoverable exceptions to propagate rather than obscuring them.