Python decorators let you extend or change a callable without editing the callable's main body. They keep cross-cutting behavior such as logging, access checks, timing, caching, and validation close to the function declaration. A decorator receives a function or class and returns the object that will replace its original name.
You should use decorators when the extra behavior applies consistently and the decorated function remains easy to understand. For one-off logic, a normal function call is often clearer.
How Python Decorators Work
Functions are first-class objects in Python. You can pass them to another function, return them, and assign them to variables. The @ syntax performs that transformation when Python executes the definition.
Equivalent forms:
# Apply the decorator with @ syntax.
@audit
def save_order():
return "Order saved"
# This assignment performs the same transformation.
def save_invoice():
return "Invoice saved"
save_invoice = audit(save_invoice)
Create a Basic Decorator
A wrapper accepts any arguments, adds behavior, and calls the original function. Return the wrapper from the decorator.
Example:
def announce(function):
def wrapper(*args, **kwargs):
# Run extra behavior before the original function.
print(f"Calling {function.__name__}")
result = function(*args, **kwargs)
# Return the original result to the caller.
return result
return wrapper
@announce
def greet(name):
return f"Hello, {name}!"
print(greet("Aarav"))
The flexible *args and **kwargs parameters allow the wrapper to handle different function signatures.
Preserve Function Metadata
A plain wrapper hides metadata such as the original name, docstring, annotations, and signature information. Apply functools.wraps() to copy supported metadata and add the useful __wrapped__ reference.
from functools import wraps
def measure(function):
@wraps(function)
def wrapper(*args, **kwargs):
# Keep this example simple by counting calls.
wrapper.calls += 1
return function(*args, **kwargs)
wrapper.calls = 0
return wrapper
@measure
def calculate_total(price, tax):
"""Return a price after tax."""
return price * (1 + tax)
print(calculate_total(1200, 0.18))
print(calculate_total.__name__) # calculate_total
Use @wraps(function) in every decorator that returns a wrapper. Debuggers, documentation tools, and introspection code can then identify the original callable.
Create a Decorator with Arguments
A parameterized decorator needs one more function level. The outer function receives configuration, the middle function receives the decorated callable, and the wrapper handles each call.
from functools import wraps
def repeat(times):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = None
# Call the function the requested number of times.
for _ in range(times):
result = function(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def notify(message):
print(message)
notify("Backup completed")
Stack Multiple Decorators
Python applies stacked decorators from the bottom upward. In the next example, validate wraps the function first, and log wraps that result.
@log
@validate
def create_user(name):
# validate runs inside log.
return {"name": name}
# The structure is log(validate(create_user)).
Decorator order matters when wrappers transform inputs, catch exceptions, or control access. Keep the stack short and document the intended order.
Decorate Methods and Classes
Python includes familiar method decorators. A normal instance method receives self, @classmethod receives the class as cls, and @staticmethod receives neither automatically. The @property decorator exposes a method through attribute syntax.
| Decorator | Purpose |
|---|---|
| @classmethod | Creates an alternative constructor or class-level operation |
| @staticmethod | Places a related utility in the class namespace |
| @property | Provides controlled attribute-style access |
class Employee:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
# Calculate the value whenever it is requested.
return f"{self.first_name} {self.last_name}"
employee = Employee("Olivia", "Brown")
print(employee.full_name)
Handle Async Functions Carefully
A synchronous wrapper around an async function only receives a coroutine object. Create an async wrapper and await the original callable when the decorator targets coroutine functions.
from functools import wraps
async def fetch_profile(user_id):
# A real function would await network I/O here.
return {"id": user_id}
def async_audit(function):
@wraps(function)
async def wrapper(*args, **kwargs):
print("Async call started")
return await function(*args, **kwargs)
return wrapper
Common Decorator Mistakes
- Calling the original function while defining the decorator instead of inside the wrapper.
- Forgetting to return the original result.
- Dropping positional or keyword arguments.
- Sharing mutable decorator state without considering concurrent calls.
- Hiding important behavior behind too many decorator layers.
Conclusion
Python decorators transform callables at definition time and provide a compact way to reuse behavior. Build wrappers with flexible arguments, preserve metadata with functools.wraps(), understand bottom-up stacking, and keep hidden behavior limited. These practices make decorators useful without making your program difficult to trace.