|
| 1 | +--- |
| 2 | +title: 'Python Decorators - Simple Patterns to Level Up Your Code - Python Cheatsheet' |
| 3 | +description: Decorators are one of Python's most elegant features, allowing you to modify the behavior of functions or methods in a clean and readable way. |
| 4 | +date: Aug 16, 2025 |
| 5 | +updated: Aug 16, 2025 |
| 6 | +tags: python, intermediate, beta |
| 7 | +socialImage: /blog/python-decorators.jpg |
| 8 | +--- |
| 9 | + |
| 10 | +<route lang="yaml"> |
| 11 | +meta: |
| 12 | + layout: article |
| 13 | + title: 'Python Decorators - Simple Patterns to Level Up Your Code' |
| 14 | + description: Decorators are one of Python's most elegant features, allowing you to modify the behavior of functions or methods in a clean and readable way. |
| 15 | + date: Aug 16, 2025 |
| 16 | + updated: Aug 16, 2025 |
| 17 | + socialImage: /blog/python-decorators.jpg |
| 18 | + tags: python, intermediate, beta |
| 19 | +</route> |
| 20 | + |
| 21 | +<blog-title-header :frontmatter="frontmatter" title="Python Decorators: Simple Patterns to Level Up Your Code" /> |
| 22 | + |
| 23 | +You know that feeling when you see `@something` above a function and wonder what black magic is happening? I've been there too. Decorators might look intimidating, but they're actually one of Python's most elegant features once you understand the basics. |
| 24 | + |
| 25 | +Think of decorators as gift wrapping for your functions. The function inside stays the same, but the decorator adds a nice bow on top – extra functionality without changing the original code. |
| 26 | + |
| 27 | +## The Simplest Decorator |
| 28 | + |
| 29 | +Let's start with the most basic example to understand what's happening: |
| 30 | + |
| 31 | +```python |
| 32 | +def my_decorator(func): |
| 33 | + def wrapper(): |
| 34 | + print("Something happens before!") |
| 35 | + func() |
| 36 | + print("Something happens after!") |
| 37 | + return wrapper |
| 38 | + |
| 39 | +@my_decorator |
| 40 | +def say_hello(): |
| 41 | + print("Hello!") |
| 42 | + |
| 43 | +say_hello() |
| 44 | +# Something happens before! |
| 45 | +# Hello! |
| 46 | +# Something happens after! |
| 47 | +``` |
| 48 | + |
| 49 | +That's it! A decorator is just a function that takes another function and wraps it with extra behavior. The `@my_decorator` syntax is just a cleaner way of writing `say_hello = my_decorator(say_hello)`. |
| 50 | + |
| 51 | +## Your First Useful Decorator: Timer |
| 52 | + |
| 53 | +Here's a decorator you'll actually want to use – one that tells you how long your functions take to run: |
| 54 | + |
| 55 | +```python |
| 56 | +import time |
| 57 | +import functools |
| 58 | + |
| 59 | +def timer(func): |
| 60 | + @functools.wraps(func) # Preserves the original function's name and docs |
| 61 | + def wrapper(*args, **kwargs): |
| 62 | + start = time.time() |
| 63 | + result = func(*args, **kwargs) |
| 64 | + end = time.time() |
| 65 | + print(f"{func.__name__} took {end - start:.4f} seconds") |
| 66 | + return result |
| 67 | + return wrapper |
| 68 | + |
| 69 | +@timer |
| 70 | +def slow_function(): |
| 71 | + time.sleep(1) |
| 72 | + return "Done!" |
| 73 | + |
| 74 | +result = slow_function() |
| 75 | +# slow_function took 1.0041 seconds |
| 76 | +print(result) # Done! |
| 77 | +``` |
| 78 | + |
| 79 | +Notice how we use `*args` and `**kwargs`? This makes our decorator work with any function, regardless of how many arguments it takes. |
| 80 | + |
| 81 | +## Debug Your Code: Logger Decorator |
| 82 | + |
| 83 | +When you're trying to figure out what's going wrong, this decorator is incredibly handy: |
| 84 | + |
| 85 | +```python |
| 86 | +def debug(func): |
| 87 | + @functools.wraps(func) |
| 88 | + def wrapper(*args, **kwargs): |
| 89 | + args_str = ', '.join(repr(arg) for arg in args) |
| 90 | + kwargs_str = ', '.join(f"{k}={v!r}" for k, v in kwargs.items()) |
| 91 | + all_args = ', '.join(filter(None, [args_str, kwargs_str])) |
| 92 | + print(f"Calling {func.__name__}({all_args})") |
| 93 | + |
| 94 | + result = func(*args, **kwargs) |
| 95 | + print(f"{func.__name__} returned {result!r}") |
| 96 | + return result |
| 97 | + return wrapper |
| 98 | + |
| 99 | +@debug |
| 100 | +def add_numbers(a, b, multiply_by=1): |
| 101 | + return (a + b) * multiply_by |
| 102 | + |
| 103 | +result = add_numbers(5, 3, multiply_by=2) |
| 104 | +# Calling add_numbers(5, 3, multiply_by=2) |
| 105 | +# add_numbers returned 16 |
| 106 | +``` |
| 107 | + |
| 108 | + |
| 109 | +## Control Access: Authentication Decorator |
| 110 | + |
| 111 | +Want to make sure only certain users can run a function? Here's how: |
| 112 | + |
| 113 | +```python |
| 114 | +def requires_auth(func): |
| 115 | + @functools.wraps(func) |
| 116 | + def wrapper(*args, **kwargs): |
| 117 | + # In a real app, you'd check actual authentication |
| 118 | + user_logged_in = True # This would come from your auth system |
| 119 | + |
| 120 | + if not user_logged_in: |
| 121 | + return "Access denied! Please log in." |
| 122 | + |
| 123 | + return func(*args, **kwargs) |
| 124 | + return wrapper |
| 125 | + |
| 126 | +@requires_auth |
| 127 | +def delete_everything(): |
| 128 | + return "💥 Everything deleted! (just kidding)" |
| 129 | + |
| 130 | +result = delete_everything() |
| 131 | +print(result) # 💥 Everything deleted! (just kidding) |
| 132 | +``` |
| 133 | + |
| 134 | + |
| 135 | +## Speed Things Up: Cache Decorator |
| 136 | + |
| 137 | +If you have a function that does expensive calculations with the same inputs, cache the results: |
| 138 | + |
| 139 | +```python |
| 140 | +def cache(func): |
| 141 | + cached_results = {} |
| 142 | + |
| 143 | + @functools.wraps(func) |
| 144 | + def wrapper(*args): |
| 145 | + if args in cached_results: |
| 146 | + print(f"Cache hit for {func.__name__}{args}") |
| 147 | + return cached_results[args] |
| 148 | + |
| 149 | + print(f"Computing {func.__name__}{args}") |
| 150 | + result = func(*args) |
| 151 | + cached_results[args] = result |
| 152 | + return result |
| 153 | + |
| 154 | + return wrapper |
| 155 | + |
| 156 | +@cache |
| 157 | +def fibonacci(n): |
| 158 | + if n < 2: |
| 159 | + return n |
| 160 | + return fibonacci(n - 1) + fibonacci(n - 2) |
| 161 | + |
| 162 | +print(fibonacci(10)) |
| 163 | +# Computing fibonacci(10) |
| 164 | +# Computing fibonacci(9) |
| 165 | +# Computing fibonacci(8) |
| 166 | +# ... (lots of computation) |
| 167 | +# Cache hit for fibonacci(2) |
| 168 | +# Cache hit for fibonacci(3) |
| 169 | +# ... (cache hits) |
| 170 | +# 55 |
| 171 | +``` |
| 172 | + |
| 173 | + |
| 174 | +## Retry Failed Operations |
| 175 | + |
| 176 | +Sometimes functions fail due to network issues or temporary problems. This decorator automatically retries: |
| 177 | + |
| 178 | +```python |
| 179 | +import random |
| 180 | + |
| 181 | +def retry(max_attempts=3): |
| 182 | + def decorator(func): |
| 183 | + @functools.wraps(func) |
| 184 | + def wrapper(*args, **kwargs): |
| 185 | + for attempt in range(max_attempts): |
| 186 | + try: |
| 187 | + return func(*args, **kwargs) |
| 188 | + except Exception as e: |
| 189 | + print(f"Attempt {attempt + 1} failed: {e}") |
| 190 | + if attempt == max_attempts - 1: |
| 191 | + print("All attempts failed!") |
| 192 | + raise |
| 193 | + return None |
| 194 | + return wrapper |
| 195 | + return decorator |
| 196 | + |
| 197 | +@retry(max_attempts=3) |
| 198 | +def unreliable_api_call(): |
| 199 | + if random.random() < 0.7: # 70% chance of failure |
| 200 | + raise Exception("Network error") |
| 201 | + return "Success!" |
| 202 | + |
| 203 | +# This will retry up to 3 times if it fails |
| 204 | +result = unreliable_api_call() |
| 205 | +``` |
| 206 | + |
| 207 | + |
| 208 | +## Rate Limiting: Slow Down Your Code |
| 209 | + |
| 210 | +Sometimes you need to be gentle with APIs or databases: |
| 211 | + |
| 212 | +```python |
| 213 | +import time |
| 214 | + |
| 215 | +def rate_limit(seconds): |
| 216 | + def decorator(func): |
| 217 | + last_called = [^0] # Use list to store mutable value |
| 218 | + |
| 219 | + @functools.wraps(func) |
| 220 | + def wrapper(*args, **kwargs): |
| 221 | + elapsed = time.time() - last_called |
| 222 | + if elapsed < seconds: |
| 223 | + time.sleep(seconds - elapsed) |
| 224 | + |
| 225 | + last_called = time.time() |
| 226 | + return func(*args, **kwargs) |
| 227 | + |
| 228 | + return wrapper |
| 229 | + return decorator |
| 230 | + |
| 231 | +@rate_limit(1) # At most once per second |
| 232 | +def call_api(): |
| 233 | + print(f"API called at {time.time():.2f}") |
| 234 | + |
| 235 | +# These will be spaced out by 1 second each |
| 236 | +call_api() |
| 237 | +call_api() |
| 238 | +call_api() |
| 239 | +``` |
| 240 | + |
| 241 | + |
| 242 | +## Validate Your Inputs |
| 243 | + |
| 244 | +Make sure your functions get the right types of data: |
| 245 | + |
| 246 | +```python |
| 247 | +def validate_types(**expected_types): |
| 248 | + def decorator(func): |
| 249 | + @functools.wraps(func) |
| 250 | + def wrapper(*args, **kwargs): |
| 251 | + # Get function parameter names |
| 252 | + import inspect |
| 253 | + sig = inspect.signature(func) |
| 254 | + bound_args = sig.bind(*args, **kwargs) |
| 255 | + bound_args.apply_defaults() |
| 256 | + |
| 257 | + for param_name, expected_type in expected_types.items(): |
| 258 | + if param_name in bound_args.arguments: |
| 259 | + value = bound_args.arguments[param_name] |
| 260 | + if not isinstance(value, expected_type): |
| 261 | + raise TypeError( |
| 262 | + f"{param_name} must be {expected_type.__name__}, " |
| 263 | + f"got {type(value).__name__}" |
| 264 | + ) |
| 265 | + |
| 266 | + return func(*args, **kwargs) |
| 267 | + return wrapper |
| 268 | + return decorator |
| 269 | + |
| 270 | +@validate_types(name=str, age=int) |
| 271 | +def create_user(name, age): |
| 272 | + return f"User {name}, age {age}" |
| 273 | + |
| 274 | +# This works |
| 275 | +user1 = create_user("Alice", 25) |
| 276 | +print(user1) # User Alice, age 25 |
| 277 | + |
| 278 | +# This raises TypeError |
| 279 | +try: |
| 280 | + user2 = create_user("Bob", "twenty-five") |
| 281 | +except TypeError as e: |
| 282 | + print(e) # age must be int, got str |
| 283 | +``` |
| 284 | + |
| 285 | + |
| 286 | +## When to Use Each Decorator |
| 287 | + |
| 288 | +| Decorator Type | Best For | Example Use Cases | |
| 289 | +| :--------------- | :----------------------------- | :------------------------------------------------ | |
| 290 | +| **Timer** | Performance monitoring | Finding slow functions, optimization | |
| 291 | +| **Debug/Logger** | Development \& troubleshooting | Understanding function calls, debugging | |
| 292 | +| **Auth** | Security \& access control | Protecting admin functions, user permissions | |
| 293 | +| **Cache** | Expensive computations | Database queries, API calls, complex calculations | |
| 294 | +| **Retry** | Unreliable operations | Network requests, file operations | |
| 295 | +| **Rate Limit** | Controlling frequency | API calls, preventing spam | |
| 296 | +| **Validation** | Data integrity | User input, API parameters | |
| 297 | + |
| 298 | +## Tips for Using Decorators |
| 299 | + |
| 300 | +**Always use `@functools.wraps`** – This preserves the original function's name and documentation, making debugging easier. |
| 301 | + |
| 302 | +**Keep them simple** – If your decorator is getting complex, consider if it should be a class or separate function instead. |
| 303 | + |
| 304 | +**Think about order** – When stacking decorators, the one closest to the function runs first: |
| 305 | + |
| 306 | +```python |
| 307 | +@timer |
| 308 | +@debug |
| 309 | +def my_function(): |
| 310 | + pass |
| 311 | + |
| 312 | +# This is the same as: |
| 313 | +# my_function = timer(debug(my_function)) |
| 314 | +``` |
| 315 | + |
| 316 | +**Don't overuse them** – Decorators are powerful, but too many can make code hard to follow. |
| 317 | + |
| 318 | +## Key Takeaways |
| 319 | + |
| 320 | +Decorators let you add functionality to functions without changing their code. They're perfect for cross-cutting concerns like timing, logging, authentication, and caching. |
| 321 | + |
| 322 | +Start with the simple patterns shown here. Once you're comfortable, you can create more sophisticated decorators for your specific needs. The key is understanding that decorators are just functions that wrap other functions – everything else is just clever applications of that basic concept. |
| 323 | + |
| 324 | +Want to practice? Try adding the `@timer` decorator to some of your existing functions and see which ones are slower than you expected. You might be surprised at what you discover! |
0 commit comments