Exception Basics
try/except/else/finally
try:
# Code that might raise exception
result = 10 / 0
except ZeroDivisionError as e:
# Handle specific exception
print(f'Error: {e}')
else:
# Runs if no exception occurred
print(f'Result: {result}')
finally:
# Always runs (cleanup code)
print('Done')
# Output:
# Error: division by zero
# Done
Common Built-in Exceptions
# ValueError - wrong value
int('hello') # ValueError: invalid literal
# TypeError - wrong type
'hello' + 1 # TypeError: can only concatenate str to str
# KeyError - missing dictionary key
{'a': 1}['b'] # KeyError: 'b'
# IndexError - list index out of range
[1, 2][5] # IndexError: list index out of range
# FileNotFoundError - file doesn't exist
open('nonexistent.txt') # FileNotFoundError
# AttributeError - attribute doesn't exist
'hello'.nonexistent # AttributeError
# ImportError - module not found
import nonexistent # ImportError
# RuntimeError - generic runtime error
raise RuntimeError('Something went wrong')
Catching Multiple Exceptions
try:
# Some code
pass
except (ValueError, TypeError) as e:
# Handle either exception
print(f'Error: {e}')
except ZeroDivisionError:
# Handle different exception
print('Division by zero')
except Exception as e:
# Catch all other exceptions (use sparingly!)
print(f'Unexpected error: {e}')
Exception Hierarchy
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- ArithmeticError
| +-- ZeroDivisionError
| +-- OverflowError
| +-- FloatingPointError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- ValueError
+-- TypeError
+-- AttributeError
+-- OSError
+-- FileNotFoundError
+-- PermissionError
+-- TimeoutError
Custom Exceptions
Creating Custom Exceptions
class AppError(Exception):
"""Base exception for application."""
pass
class ValidationError(AppError):
"""Raised when validation fails."""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f'{field}: {message}')
class NotFoundError(AppError):
"""Raised when resource not found."""
def __init__(self, resource, identifier):
self.resource = resource
self.identifier = identifier
super().__init__(f'{resource} with id {identifier} not found')
# Usage
def validate_age(age):
if not isinstance(age, int):
raise ValidationError('age', 'Must be an integer')
if age < 0 or age > 150:
raise ValidationError('age', 'Must be between 0 and 150')
return True
try:
validate_age(-5)
except ValidationError as e:
print(f'Validation failed: {e}')
print(f'Field: {e.field}')
print(f'Message: {e.message}')
Exception Chaining
class DatabaseError(Exception):
pass
class ConnectionError(DatabaseError):
pass
try:
try:
open('config.txt')
except FileNotFoundError as e:
# Chain the original exception
raise DatabaseError('Failed to load config') from e
except DatabaseError as e:
print(f'Error: {e}')
print(f'Original: {e.__cause__}')
Best Practices
# ✅ Do: Be specific
try:
value = my_dict[key]
except KeyError:
value = default
# ❌ Don't: Catch everything
try:
value = my_dict[key]
except Exception:
value = default
# ✅ Do: Log exceptions
import logging
try:
risky_operation()
except Exception as e:
logging.exception('Operation failed')
raise # Re-raise if needed
# ✅ Do: Use custom exceptions for domain errors
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(
f'Insufficient funds: balance={balance}, amount={amount}'
)
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
Exception Patterns
EAFP vs LBYL
# LBYL: Look Before You Leap
if key in my_dict:
value = my_dict[key]
else:
value = default
# EAFP: Easier to Ask Forgiveness than Permission
try:
value = my_dict[key]
except KeyError:
value = default
# Python prefers EAFP - it's more Pythonic
Exception as Flow Control
# ✅ Good: Exceptions for exceptional cases
def divide(a, b):
if b == 0:
raise ValueError('Cannot divide by zero')
return a / b
# ❌ Bad: Exceptions for normal flow
# Don't use exceptions for regular conditional logic
try:
result = divide(10, 0)
except ValueError:
result = None # This is expected, not exceptional
# ✅ Better: Return None or use Optional
from typing import Optional
def divide(a: float, b: float) -> Optional[float]:
if b == 0:
return None
return a / b
Context Managers and Exceptions
class ManagedResource:
def __enter__(self):
print('Acquiring')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('Releasing')
# Return True to suppress exception
# Return False to propagate
if exc_type is TimeoutError:
print('Timeout - suppressed')
return True # Suppress TimeoutError
return False # Propagate other exceptions
with ManagedResource():
raise TimeoutError('Timed out') # Suppressed
with ManagedResource():
raise ValueError('Bad value') # Propagated
Raising Exceptions
# Raise with message
raise ValueError('Invalid value')
# Raise with chaining
try:
open('missing.txt')
except FileNotFoundError as e:
raise RuntimeError('Config error') from e
# Re-raise current exception
try:
risky_operation()
except Exception:
log_error()
raise # Re-raise with original traceback
# Raise from nothing (suppress context)
raise RuntimeError('Bad') from None