Try/Except Basics
Python uses exceptions for error handling rather than error codes. When something goes wrong, Python raises an exception. You catch exceptions using try/except blocks. This approach leads to cleaner code with a clear separation between the normal flow and error handling.
# Basic try/except
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
# Catching multiple exceptions
try:
value = int("not a number")
except ValueError:
print("Invalid number format")
except TypeError:
print("Wrong type")
# Catching multiple exceptions in one clause
try:
# some operation
data = {"key": "value"}
result = data["missing"] + 1
except (KeyError, TypeError) as e:
print(f"Error: {e}")
# The full try/except/else/finally
try:
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("File not found!")
content = ""
else:
# Runs only if no exception occurred
print(f"Read {len(content)} characters")
finally:
# Always runs, even if exception occurred
print("Cleanup complete")
Exception Hierarchy
Python's exception hierarchy is a class tree. All exceptions inherit from BaseException, with Exception being the base for most errors you will catch. Understanding the hierarchy helps you catch the right exceptions at the right level of specificity.
Common Exception Types
- ValueError: Correct type but inappropriate value (e.g., int("abc"))
- TypeError: Operation on wrong type (e.g., "str" + 5)
- KeyError: Dictionary key not found
- IndexError: Sequence index out of range
- AttributeError: Object has no such attribute
- FileNotFoundError: File or directory does not exist
- IOError/OSError: System-related error (file I/O, network, etc.)
- RuntimeError: Generic runtime error
Raising Exceptions
# Raise an exception
def validate_age(age):
if not isinstance(age, int):
raise TypeError(f"Age must be an integer, got {type(age).__name__}")
if age < 0 or age > 150:
raise ValueError(f"Age must be between 0 and 150, got {age}")
return age
# Re-raise an exception
try:
result = some_risky_operation()
except ValueError:
print("Logging the error...")
raise # Re-raises the caught exception
# Exception chaining
try:
data = load_config("config.json")
except FileNotFoundError as e:
raise RuntimeError("Failed to initialize application") from e
Custom Exceptions
Creating custom exception classes lets you define domain-specific errors that make your code more readable and your error handling more precise. Always inherit from Exception (not BaseException).
# Custom exception hierarchy
class AppError(Exception):
"""Base exception for our application."""
pass
class ValidationError(AppError):
"""Raised when input validation fails."""
def __init__(self, field: str, message: str):
self.field = field
self.message = message
super().__init__(f"Validation error on '{field}': {message}")
class NotFoundError(AppError):
"""Raised when a resource is not found."""
def __init__(self, resource: str, identifier):
self.resource = resource
self.identifier = identifier
super().__init__(f"{resource} with id '{identifier}' not found")
class AuthenticationError(AppError):
"""Raised when authentication fails."""
pass
# Using custom exceptions
def create_user(name: str, email: str):
if not name:
raise ValidationError("name", "Name cannot be empty")
if "@" not in email:
raise ValidationError("email", "Invalid email format")
return {"name": name, "email": email}
try:
user = create_user("", "invalid")
except ValidationError as e:
print(f"Field: {e.field}")
print(f"Message: {e.message}")
except AppError as e:
print(f"Application error: {e}")
Context Managers
Context managers guarantee cleanup using the with statement. You can create your own using the __enter__/__exit__ protocol or the @contextmanager decorator.
from contextlib import contextmanager
import time
# Class-based context manager
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
print(f"Elapsed: {self.elapsed:.4f} seconds")
return False # Don't suppress exceptions
with Timer() as t:
total = sum(range(1_000_000))
print(f"Result: {total}")
# Generator-based context manager (simpler)
@contextmanager
def temporary_directory():
import tempfile
import shutil
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir
finally:
shutil.rmtree(tmpdir)
with temporary_directory() as tmpdir:
print(f"Working in {tmpdir}")
# Directory is automatically cleaned up
Key Takeaways
- Catch specific exceptions: Never use bare except: clauses
- Use else and finally: else for success logic, finally for cleanup
- Create custom exceptions: Build a hierarchy for your application's errors
- Context managers: Use with statements for any resource that needs cleanup