Overview Exception handling is often the difference between a brittle script and a resilient production application. While many beginners view errors as bugs to be squashed, seasoned developers recognize them as predictable runtime conditions like lost internet connections or missing files. This guide explores how to move beyond basic `try-except` blocks by implementing multi-layered error handling, custom context managers, and advanced decorators to ensure your code remains maintainable and resource-safe. Prerequisites To get the most out of this tutorial, you should have a firm grasp of Python fundamentals, including function definitions and classes. Familiarity with Flask or basic SQL will help when reviewing the database examples, though the core logic applies to any Python project. Key Libraries & Tools - **Flask**: A lightweight WSGI web application framework used here to demonstrate API error responses. - **SQLite3**: A C-language library that implements a small, fast, self-contained SQL database engine. - **JSON**: Used for structuring data exchange between the backend and the client. Code Walkthrough Multi-Layered Exception Handling Instead of letting low-level database errors leak into your API layer, wrap them in custom exceptions. This keeps your interface clean and decoupled from the underlying storage technology. ```python class NotFoundError(Exception): pass class NotAuthorizedError(Exception): pass def fetch_blog(id): try: conn = sqlite3.connect('app.db') cursor = conn.cursor() cursor.execute("SELECT * FROM blogs WHERE id=?", (id,)) result = cursor.fetchone() if result is None: raise NotFoundError() # Logic to check if blog is public return result except sqlite3.OperationalError as e: print(f"Database error: {e}") raise NotFoundError() finally: conn.close() ``` In this snippet, we use a `finally` block to ensure the database connection closes regardless of success or failure. This prevents resource leaks. Custom Context Managers Managing resources like database connections manually is error-prone. We can encapsulate this logic using the `__enter__` and `__exit__` methods. ```python class SQLite: def __init__(self, filename): self.filename = filename def __enter__(self): self.connection = sqlite3.connect(self.filename) return self.connection.cursor() def __exit__(self, type, value, traceback): self.connection.close() Usage with SQLite('app.db') as cursor: cursor.execute("SELECT * FROM blogs") data = cursor.fetchall() ``` The `with` statement ensures the `__exit__` method runs even if an exception occurs inside the block. Syntax Notes Notice the `except Exception as e` pattern. This allows you to capture the exception object to log specific error messages. In the context manager, the `__exit__` method requires four arguments: `self`, `type`, `value`, and `traceback`. Even if you don't use the error details, the signature must be exact for Python to recognize it as a valid exit handler. Practical Examples Decorators offer a powerful way to add "retry" logic or automatic logging to functions. A **Retry Decorator** can catch transient network failures and re-run the function after a short delay, while a **Logging Decorator** can automatically write stack traces to a file without cluttering the business logic with print statements. Tips & Gotchas - **Avoid Naked Excepts**: Never use `except:` without specifying an exception type. It catches even `SystemExit` and `KeyboardInterrupt`, making it impossible to stop your program with `Ctrl+C`. - **Hidden Control Flow**: Exceptions create a second, invisible path through your code. Keep your `try` blocks as small as possible to ensure you know exactly which line failed.
Lisp
Languages
- Mar 26, 2021