Skip to content

Exception Handling

Python uses an exception mechanism to handle runtime errors, preventing a program from crashing due to unexpected failures. This article covers everything from basic exception catching to the exception group syntax introduced in Python 3.11+.

What Are Exceptions

Exceptions fall into two categories:

  • Syntax errors (SyntaxError): Code that does not conform to Python’s grammar rules; reported at parse time before any code runs.
  • Runtime errors: Code is syntactically correct but encounters a problem during execution (e.g., division by zero, accessing a non-existent key).
# Syntax error: the code never executes
# if x > 0  # SyntaxError: expected ':'

# Runtime errors
x = 10 / 0        # ZeroDivisionError
d = {}
d["key"]          # KeyError

Basic try-except Syntax

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")

Multiple except Branches

try:
    value = int("abc")
except ValueError:
    print("Invalid integer format")
except TypeError:
    print("Type error")

Catching Multiple Exception Types at Once

try:
    data = {}
    print(data["key"])
except (KeyError, IndexError) as e:
    print(f"Lookup failed: {e}")

Catching All Exceptions

try:
    risky_operation()
except Exception as e:
    # Exception is the base class for all non-system-exit exceptions
    print(f"An error occurred: {type(e).__name__}: {e}")
Avoid bare except: (with no exception type). It catches everything, including KeyboardInterrupt and SystemExit, which prevents the program from exiting normally.

else and finally

try:
    f = open("data.txt", "r")
    content = f.read()
except FileNotFoundError as e:
    print(f"File not found: {e}")
except PermissionError as e:
    print(f"Permission denied: {e}")
else:
    # Executes only when the try block raises no exception
    print(f"File content: {content}")
finally:
    # Always executes, with or without an exception; typically used to release resources
    print("Operation complete")

A better approach is to use the with statement for automatic resource management (see with Statement and Context Managers):

try:
    with open("data.txt") as f:
        content = f.read()
except FileNotFoundError:
    print("File not found")

Raising Exceptions

def set_age(age: int) -> None:
    if not isinstance(age, int):
        raise TypeError(f"age must be an int, got {type(age).__name__}")
    if age < 0 or age > 150:
        raise ValueError(f"age out of valid range: {age}")

# Re-raise inside an except block
try:
    set_age(-1)
except ValueError as e:
    print(f"Caught: {e}")
    raise   # re-raise the same exception (preserves the original traceback)

Custom Exceptions

class AppError(Exception):
    """Application-level exception base class"""
    pass

class DatabaseError(AppError):
    def __init__(self, message: str, code: int = 0):
        super().__init__(message)
        self.code = code

    def __str__(self) -> str:
        return f"[DB-{self.code}] {super().__str__()}"

raise DatabaseError("Connection timed out", code=503)

assert Statements

Assertions are used during development to verify internal assumptions. Do not use them for user-input validation — assertions can be disabled in production with the -O flag:

def divide(a: float, b: float) -> float:
    assert b != 0, "Divisor cannot be zero"
    return a / b

# AssertionError: Divisor cannot be zero
divide(10, 0)

Exception Chaining (Python 3)

try:
    open("missing.txt")
except FileNotFoundError as e:
    raise RuntimeError("Initialization failed") from e
    # The output shows both exceptions and their relationship

# Use `from None` to suppress the original exception
try:
    open("missing.txt")
except FileNotFoundError:
    raise RuntimeError("Configuration file missing") from None

Exception Groups and except* (Python 3.11+)

Python 3.11 introduced ExceptionGroup and the except* syntax, designed for concurrent scenarios (e.g., asyncio task groups), allowing multiple exceptions to be raised and handled simultaneously:

# Create an exception group
def fetch_data():
    raise ExceptionGroup(
        "Network error group",
        [
            ConnectionError("Failed to connect to server A"),
            TimeoutError("Request to server B timed out"),
            ExceptionGroup(
                "Sub-error group",
                [OSError("Disk I/O error")]
            )
        ]
    )

# except* matches all exceptions of the given type within the group
try:
    fetch_data()
except* ConnectionError as eg:
    print(f"Connection errors: {eg.exceptions}")
except* TimeoutError as eg:
    print(f"Timeout errors: {eg.exceptions}")
except* differs from regular except: it does not stop at the first match. Instead it traverses the entire exception group, collecting all matching exceptions into eg.exceptions. Any exceptions not caught by any except* clause are re-raised.

Retrieving Exception Details

import sys
import traceback

try:
    1 / 0
except ZeroDivisionError as e:
    # Method 1: print the exception message directly
    print(type(e).__name__, e)

    # Method 2: print the full traceback
    traceback.print_exc()

    # Method 3: get the traceback as a string
    tb_str = traceback.format_exc()

    # Method 4: get the line number via sys.exc_info()
    exc_type, exc_value, exc_tb = sys.exc_info()
    print(f"File: {exc_tb.tb_frame.f_code.co_filename}")
    print(f"Line: {exc_tb.tb_lineno}")

Common Built-in Exception Types

ExceptionWhen It Is Raised
ValueErrorArgument value is invalid (e.g., int("abc"))
TypeErrorType mismatch (e.g., 1 + "a")
KeyErrorDictionary key does not exist
IndexErrorList index out of range
AttributeErrorAccessing a non-existent attribute
ImportErrorModule import failed
FileNotFoundErrorFile does not exist (subclass of OSError)
PermissionErrorInsufficient permissions (subclass of OSError)
ZeroDivisionErrorDivision by zero
RecursionErrorMaximum recursion depth exceeded
StopIterationIterator is exhausted
RuntimeErrorGeneral runtime error
NotImplementedErrorAbstract method not implemented
MemoryErrorOut of memory
KeyboardInterruptUser pressed Ctrl+C
SystemExitsys.exit() was called

Exception hierarchy:

BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ArithmeticError
    │   └── ZeroDivisionError
    ├── LookupError
    │   ├── KeyError
    │   └── IndexError
    ├── OSError
    │   ├── FileNotFoundError
    │   └── PermissionError
    ├── ValueError
    ├── TypeError
    └── RuntimeError
        └── RecursionError
Last updated on