Decorators Done Right
Introduction
Welcome to the third course in the Advanced Python Language Features path! By completing the previous two courses on the Python Data Model and Class Machinery, you've built a solid foundation in designing custom types, implementing protocols, and creating extensible systems. Now, we're ready to explore functional programming patterns and modern control flow structures that will make your code more expressive and declarative.
In this course, we'll master four essential patterns: decorators, generic functions with single dispatch, structural pattern matching, and composable error handling. Today's lesson focuses on decorators done right: we'll build a configurable retry mechanism with exponential backoff and jitter, learning how to preserve function metadata, control exception handling, and leverage advanced typing features for type-safe decorator factories. By the end of this lesson, you'll be able to write decorators that feel professional and production-ready.
Why Decorators Matter
Decorators are powerful tools for adding reusable behavior to functions without modifying their core logic. However, many decorators in the wild have issues: they lose function metadata, break type checking, or lack configurability. A production-grade decorator must handle these concerns (and potentially more) gracefully.
Consider a retry decorator for network operations. A naive implementation might catch all exceptions and retry indefinitely, which could mask permanent failures or overwhelm a failing service. A robust solution needs:
- Exponential backoff to reduce load on struggling systems
- Jitter to prevent synchronized retry storms
- Selective exception handling to distinguish transient from permanent failures
- Preserved function identity so debugging and introspection work correctly
- Type safety so IDEs and type checkers understand the decorated function
These requirements transform a simple wrapper into a sophisticated, reusable component.
The Basic Retry Pattern
Let's start with the simplest possible retry decorator to establish the core pattern. A retry decorator wraps a function and catches exceptions, attempting the call again after a delay:
This basic decorator tries the function up to three times, waiting one second between attempts. The @wraps(func) line preserves the original function's metadata. However, this implementation is inflexible: the retry count and delay are hardcoded, and it catches all exceptions indiscriminately.
