A decorator is simply a function that takes another function and extends its behavior. Think of it like wrapping a gift — the gift (original function) is the same, but now it has extra packaging (the decorator logic).
The Core Idea
Before we look at the @ syntax, let’s understand what’s actually happening.
original_func(), we're actually calling wrapper_func()
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function runs")
result = func(*args, **kwargs) # call the original
print("After the function runs")
return result
return wrapper
def say_hello():
print("Hello!")
# Manual decoration
say_hello = my_decorator(say_hello)
say_hello()
# Before the function runs
# Hello!
# After the function runs
The @ Syntax Sugar
The @decorator syntax is just a cleaner way to write func = decorator(func).
@my_decorator
def say_hello():
print("Hello!")
# This is EXACTLY the same as:
# say_hello = my_decorator(say_hello)
Always Use functools.wraps
Without functools.wraps, the wrapper replaces the original function’s name and docstring. This breaks introspection and debugging.
from functools import wraps
def my_decorator(func):
@wraps(func) # preserves func.__name__, func.__doc__, etc.
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
A Practical Example: Timing Decorator
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
slow_function() # "slow_function took 1.0012s"
Decorators with Arguments
If we want our decorator to accept parameters, we need an extra layer of nesting.
def repeat(n):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Manish") # prints "Hello, Manish!" three times
Stacking Decorators
We can apply multiple decorators. They run bottom to top (closest to the function first).
@decorator_a
@decorator_b
def my_func():
pass
# Same as: my_func = decorator_a(decorator_b(my_func))
Common Built-in Decorators
@property— turns a method into a read-only attribute@staticmethod— method that doesn’t needselforcls@classmethod— method that receives the class (cls) instead of the instance@functools.lru_cache— memoization (caches return values)
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
c = Circle(5)
c.area # 78.53975 — accessed like an attribute, no parentheses
In simple language, a decorator wraps a function with extra behavior. The @ syntax is just a shortcut. And always use @wraps to keep the original function’s identity intact.