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"] # KeyErrorBasic 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}")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 NoneException 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
| Exception | When It Is Raised |
|---|---|
ValueError | Argument value is invalid (e.g., int("abc")) |
TypeError | Type mismatch (e.g., 1 + "a") |
KeyError | Dictionary key does not exist |
IndexError | List index out of range |
AttributeError | Accessing a non-existent attribute |
ImportError | Module import failed |
FileNotFoundError | File does not exist (subclass of OSError) |
PermissionError | Insufficient permissions (subclass of OSError) |
ZeroDivisionError | Division by zero |
RecursionError | Maximum recursion depth exceeded |
StopIteration | Iterator is exhausted |
RuntimeError | General runtime error |
NotImplementedError | Abstract method not implemented |
MemoryError | Out of memory |
KeyboardInterrupt | User pressed Ctrl+C |
SystemExit | sys.exit() was called |
Exception hierarchy:
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── KeyError
│ └── IndexError
├── OSError
│ ├── FileNotFoundError
│ └── PermissionError
├── ValueError
├── TypeError
└── RuntimeError
└── RecursionError