✦ For everyone, free.

Practical knowledge for real and everyday life

Home

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.

AspectResponsibility
Exception ObjectEncapsulates failure or control condition data
Built-in HierarchyOrganizes exception types for matching and specialization
RaisingInstantiates and raises exceptions to signal errors
TracebacksRecords call stack frames where exception propagated
HandlersMatch exceptions by type and execute recovery code
finallyExecutes cleanup code regardless of exception occurrence
ChainingLinks related exceptions explicitly or implicitly
Exception GroupsRepresent multiple simultaneous exceptions as a single object
Custom ExceptionsDefine 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 using raise ... from ....
  • __suppress_context__: A boolean flag indicating whether to suppress display of the implicit context.
  • __notes__: A list of supplementary diagnostic strings attached via add_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 TypeDescription and Examples
BaseExceptionRoot of all exceptions.
├─ SystemExitRaised by sys.exit(), terminates the interpreter.
├─ KeyboardInterruptRaised on user interrupt (Ctrl+C).
├─ GeneratorExitRaised when a generator or coroutine is closed.
└─ ExceptionBase for ordinary exceptions.
     ├─ ArithmeticErrorBase for arithmetic errors. Subclasses include ZeroDivisionError, OverflowError, FloatingPointError.
     ├─ LookupErrorBase for lookup errors such as IndexError, KeyError.
     ├─ ImportErrorImport-related errors including ModuleNotFoundError.
     ├─ NameErrorName-related errors such as UnboundLocalError.
     ├─ OSErrorOperating system-related errors, including FileNotFoundError, PermissionError.
     ├─ RuntimeErrorGeneric runtime errors.
     ├─ SyntaxErrorSyntax errors, including IndentationError and TabError.
     ├─ WarningBase for warning categories like DeprecationWarning.
     └─ ExceptionGroupRepresents multiple exceptions raised concurrently. Inherits from Exception.
BaseExceptionGroupRepresents 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.

  • raise followed by an exception instance raises that instance directly.
  • raise followed by an exception class automatically instantiates it with no arguments and raises the new instance.
  • A bare raise re-raises the currently active exception within an exception handler.
  • Using a bare raise outside an active exception handler raises a RuntimeError.
  • raise new_exception from cause sets the explicit cause (__cause__) for exception chaining.
  • raise new_exception from None suppresses 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 None suppresses the display of implicit context (__context__) but does not delete it; __suppress_context__ is set to True to 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.

  • BaseExceptionGroup can contain any BaseException instances (including system-exiting exceptions).
  • ExceptionGroup contains only ordinary Exception instances.

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:
    • except and except* cannot coexist in the same try.
    • A matching type is mandatory in except*.
    • Matching BaseExceptionGroup subclasses directly is invalid.
    • return, break, and continue are disallowed in except* 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.