Python decorators let you modify a function’s behavior without changing its code. For example, you can just use @decorator
syntax to add logging or timing to any function.
In this tutorial, you’ll learn how decorators work, see some real-world code examples and know about some best practices to follow. If you’re new to decorators, buckle up; by the end of reading this article, you can use them like a pro.
Tip💡: Explore more advanced topics in Python like this.
Think of decorators as fancy gift wrappers for your functions. They allow you to take an existing function and extend its behaviour without modifying its source code. It’s like adding superpowers to your functions on demand!
In Python terms, decorators are just functions that modify other functions. They’re like powerful middleware that can execute code before and after the wrapped function runs. At a high level, decorators follow the decorator design pattern: they take a function, add some behaviour, and then return it.
In Python, they’re compelling because functions are first-class citizens, meaning you can pass them around and manipulate them like any other object.
Let’s start with something straightforward:
def my_first_decorator(func):
def wrapper():
print("Something is happening before the function")
func()
print("Something is happening after the function")
return wrapper
@my_first_decorator
def say_hello():
print("Hello!")
# When we call say_hello()...
say_hello()
# Output:
# Something is happening before the function
# Hello!
# Something is happening after the function
Code language: PHP (php)
💡 The @
syntax is just syntactic sugar! It’s the same as writing: say_hello = my_first_decorator(say_hello)
Let’s break down the previous example and dive into step by step what’s happening:
my_first_decorator
) that takes another function as an argument.wrapper
) that calls the original function and modifies its result.wrapper
function.@my_first_decorator
Syntax, Python essentially does this behind the scenes:These are the most common type of decorators. They are used to modify or enhance functions or methods. A function decorator is a higher-order function that takes a function as an argument and returns a new function (or a modified version of the original).
Refer to the very first example we shared earlier in this guide.
Class decorators are used to modify or enhance entire classes. A class decorator is a function that takes a class as an argument and returns a new class or a modified version of the original class.
class Cache
def __init__(self):
self.data = {}
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in self.data:
self.data[key] = func(*args, **kwargs)
return self.data[key]
return wrapper
@Cache()
def expensive_computation(n):
print("Computing...")
return n * n
print("First try:")
print(expensive_computation(10))
print("Second attempt:")
print(expensive_computation(10))
# Output
# First try:
# Computing...
# 100
# Second attempt:
# 100
Python comes with several built-in decorators that provide common functionalities. These are often used to interact with classes and methods in specific ways.
Declares a static method within a class. Static methods don’t receive an implicit first argument (self
or cls
). They are bound to the class and not the instance of the class.
class MathUtils:
@staticmethod
def add(x, y):
return x + y
print(MathUtils.add(5, 3)) # Output: 8
Code language: CSS (css)
Declares a class method. Class methods receive the class itself as the first argument, conventionally named cls
. They can modify class state that applies across all instances of the class.
class Car:
num_wheels = 4
@classmethod
def get_num_wheels(cls):
return cls.num_wheels
print(Car.get_num_wheels()) # Output: 4
Used to create getter, setter, and deleter methods for a class attribute, allowing you to treat a method like an attribute.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
print("Getting radius")
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
print("Setting radius")
self._radius = value
c = Circle(5)
print(c.radius) # Calls the getter
c.radius = 10 # Calls the setter
Code language: HTML, XML (xml)
Python decorators can take arguments if you want it to. Here’s a simple example:
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}")
greet("Bob")
# Output:
# Hello, Bob
# Hello, Bob
# Hello, Bob
Code language: PHP (php)
This is similar to what we can use to implement auth gatekeeping to some functions:
def require_auth(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
auth_token = request.headers.get('Authorization')
if not auth_token:
raise AuthError("No auth token provided")
if not is_valid_token(auth_token):
raise AuthError("Invalid token")
return func(request, *args, **kwargs)
return wrapper
@require_auth
def get_user_data(request):
return {"user": "data"}
Code language: JavaScript (javascript)
Here’s another real-world use case for retry logic. It’s often useful in scenarios like microservices where networks can often be unreliable:
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
raise e
time.sleep(delay)
return None
return wrapper
return decorator
@retry(max_attempts=3, delay=2)
def unstable_network_call():
# Simulated unstable API call
pass
Code language: PHP (php)
Here’s something I use all the time to profile my functions and optimize:
import time
from functools import wraps
def timer(func):
@wraps(func) # This preserves the original function's metadata
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.2f} seconds to run")
return result
return wrapper
@timer
def slow_function():
"""This is a slow function."""
time.sleep(1)
return "Done!"
print(slow_function())
# Output:
# slow_function took 1.00 seconds to run
# Done!
Code language: PHP (php)
⚠️ Common Mistake Alert: Forgetting @wraps(func)
will mess up your function’s metadata, causing headaches with testing and documentation tools.
# DON'T DO THIS
def decorator(func):
cache = [] # This is shared between all decorated functions!
def wrapper():
return func()
return wrapper
# DO THIS
def decorator(func):
def wrapper(cache=None):
cache = cache or [] # New cache for each call
return func()
return wrapper
Code language: PHP (php)
@decorator1
@decorator2
def function():
pass
# Is equivalent to:
function = decorator1(decorator2(function))
# decorator1 executes after decorator2
Code language: PHP (php)
If you forget to return wrapper
in your decorator, it’ll break, likely with a NoneType
error.
# Broken example - missing return statement
def broken_decorator(func):
def wrapper():
print("Doing something extra!")
func()
@broken_decorator
def greet():
print("Hello")
# This will raise an error when called
# greet()
Code language: PHP (php)
You probably picked up a few ones already if you followed thoroughly so far. However, here’s a concise list of key best practices when working with decorators:
@wraps
, to help preserve metadataDecorators are like secret ingredients in your Python cookbook. They allow you to write cleaner, more modular code by separating concerns. Remember, the key to mastering decorators is practice. Start small, experiment, and soon you’ll be wrapping functions like a pro! Also, read more on official documentaiton as well.
Happy Python programming! 🐍✨
Unlock the full potential of service workers with advanced features like push notifications, background sync, and performance optimization techniques that transform your web app into…
Learn how to integrate service workers in React, Next.js, Vue, and Angular with practical code examples and production-ready implementations for modern web applications.
Master the essential service worker caching strategies that transform web performance. Learn Cache-First, Network-First, and Stale-While-Revalidate patterns with practical examples that'll make your apps blazingly…
This website uses cookies.