Difficulty: Beginner
Explain Python exception handling. How do you create custom exceptions?
Python uses try/except/else/finally for exception handling: - try: code that might raise an exception - except: handle specific exceptions - else: runs only if no exception occurred - finally: always runs (cleanup code)
Exception hierarchy: BaseException -> Exception -> specific exceptions.
Best practices: - Catch specific exceptions, not bare except - Use else clause for code that should only run on success - Use finally for cleanup (or better: use context managers) - Create custom exceptions by inheriting from Exception
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Cannot divide by zero")
return None
except TypeError as e:
print(f"Type error: {e}")
return None
else:
# Only runs if NO exception occurred
print(f"Division successful: {result}")
return result
finally:
# ALWAYS runs, even after return
print("Division operation complete")
print(divide(10, 3))
print("---")
print(divide(10, 0))
print("---")
print(divide("10", 3))
The else block runs only on success - don't put success code in try (it might catch unrelated errors). finally always runs, even after return or exception.
# Custom exception hierarchy
class AppError(Exception):
"""Base exception for our application."""
pass
class ValidationError(AppError):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class NotFoundError(AppError):
def __init__(self, resource, resource_id):
self.resource = resource
self.resource_id = resource_id
super().__init__(f"{resource} with id {resource_id} not found")
class AuthenticationError(AppError):
pass
# Usage
def get_user(user_id):
if not isinstance(user_id, int):
raise ValidationError('user_id', 'Must be an integer')
if user_id < 0:
raise ValidationError('user_id', 'Must be positive')
if user_id > 1000:
raise NotFoundError('User', user_id)
return {'id': user_id, 'name': 'Alice'}
try:
user = get_user(9999)
except ValidationError as e:
print(f"Validation: {e.field} - {e.message}")
except NotFoundError as e:
print(f"Not found: {e.resource} #{e.resource_id}")
except AppError as e:
print(f"App error: {e}")
Custom exceptions should form a hierarchy. Catch specific exceptions first, then broader ones. Include relevant context as attributes.
# Exception chaining with 'from'
def parse_config(text):
try:
key, value = text.split('=')
return {key.strip(): int(value.strip())}
except ValueError as e:
raise ValueError(f"Invalid config line: '{text}'") from e
try:
parse_config("invalid line")
except ValueError as e:
print(f"Error: {e}")
print(f"Caused by: {e.__cause__}")
# Anti-patterns to avoid
# BAD: bare except catches everything including SystemExit, KeyboardInterrupt
# try:
# ...
# except: # NEVER do this
# pass
# BAD: catching too broad
# try:
# ...
# except Exception: # catches everything - usually too broad
# pass
# GOOD: specific exceptions
try:
value = int("not a number")
except ValueError:
value = 0 # sensible default
# GOOD: re-raise with context
try:
data = process(raw_data)
except ProcessingError:
logger.exception("Failed to process data") # logs traceback
raise # re-raises the same exception
'raise ... from e' chains exceptions, preserving the original cause. 'raise' alone re-raises the current exception. Never use bare except or catch-all-and-silence.
try/except, finally, raise, Custom Exceptions, Exception Hierarchy