
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.
What Are Decorators in Python?
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.
Your First Decorator: Baby Step
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)
The Anatomy of a Python Decorator
Let’s break down the previous example and dive into step by step what’s happening:
- We define a function (
my_first_decorator
) that takes another function as an argument. - Inside, we define a new function (
wrapper
) that calls the original function and modifies its result. - We return this new
wrapper
function. - When we use the
@my_first_decorator
Syntax, Python essentially does this behind the scenes:

Core Decorator Types
Function Decorators
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).
Key Characteristics
- Can modify arguments or return values
- Wraps a single function or method.
- Can add functionality before or after the original function’s execution.
Example:
Refer to the very first example we shared earlier in this guide.
Class Decorators: The Object-Oriented Approach
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.
Key Characteristics
- Can be used for tasks like singleton creation or automatic registration of classes.
- Wraps an entire class.
- Can add or modify class attributes or methods.
Example:
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
Built-in Decorators
Python comes with several built-in decorators that provide common functionalities. These are often used to interact with classes and methods in specific ways.
@staticmethod decorator
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)
@classmethod decorator
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
@property decorator
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)
Levelling Up – Python Decorators with Arguments!
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)
Real-World Examples Of Python Decorators
Authentication Decorator
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)
Decorator For Retry Mechanism:
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)
Timing Decorator:
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.
Common Pitfalls And How to Avoid Them
Mutable Arguments in Decorators:
# 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)
Decorators Order Matter in Python:
@decorator1
@decorator2
def function():
pass
# Is equivalent to:
function = decorator1(decorator2(function))
# decorator1 executes after decorator2
Code language: PHP (php)
Forgetting to Return the Wrapper:
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)
Python Decorators Best Practices
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:
- Always use
@wraps
, to help preserve metadata - Keep decorators focused on a single responsibility
- Document what your decorators do
- Consider making decorators optional with parameters
- Test decorated functions both with and without the decorator
Conclusion
Decorators 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! 🐍✨
Discover more from CodeSamplez.com
Subscribe to get the latest posts sent to your email.
Leave a Reply