What Are Decorators?
A decorator is a function that takes another function as input and returns a new function with added (or modified) behaviour. The @ syntax is just a clean shorthand for applying a decorator.
Before understanding decorators, you need to know three things about Python functions:
- Functions are first-class objects β they can be passed as arguments, returned from functions, and stored in variables.
- Functions can be defined inside other functions (closures).
- A function can return another function.
# Functions as first-class objects
def greet(name):
return f"Hello, {name}!"
# Assign a function to a variable
say_hello = greet
print(say_hello("Alice")) # Hello, Alice!
# Pass a function as an argument
def apply(func, value):
return func(value)
print(apply(greet, "Bob")) # Hello, Bob!
# Return a function from a function
def make_multiplier(n):
def multiply(x):
return x * n # 'n' is captured in the closure
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
Building Your First Decorator
A decorator is nothing more than a function that wraps another function. Here is the pattern without and then with the @ syntax:
# Step 1: Write a wrapper manually
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function runs")
result = func(*args, **kwargs)
print("After the function runs")
return result
return wrapper
def say_hi():
print("Hi!")
# Apply the decorator manually
say_hi = my_decorator(say_hi)
say_hi()
# Step 2: The @ syntax does the same thing β cleaner!
@my_decorator
def say_bye():
print("Bye!")
say_bye()
@my_decorator placed above a function definition is exactly equivalent to writing func = my_decorator(func) right after the function. The @ syntax just makes it visually clear that the decoration happens at definition time.
Preserving Metadata with functools.wraps
Without functools.wraps, decorated functions lose their name, docstring, and other metadata. Always use it in real-world decorators:
from functools import wraps
# Without wraps β identity is lost
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def compute():
"""Compute something important."""
pass
print(compute.__name__) # wrapper β WRONG
print(compute.__doc__) # None β WRONG
# With wraps β identity is preserved
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def compute():
"""Compute something important."""
pass
print(compute.__name__) # compute β Correct
print(compute.__doc__) # Compute something important. β Correct
Practical Decorator Examples
Timing Decorator
import time
from functools import wraps
def timer(func):
"""Measure and print how long a function takes to run."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} finished in {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
total = slow_sum(10_000_000)
print(f"Sum: {total}")
Logging Decorator
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
def log_calls(func):
"""Log every call to the decorated function with args and return value."""
@wraps(func)
def wrapper(*args, **kwargs):
arg_str = ", ".join(
[repr(a) for a in args] +
[f"{k}={v!r}" for k, v in kwargs.items()]
)
logging.info("Calling %s(%s)", func.__name__, arg_str)
result = func(*args, **kwargs)
logging.info("%s returned %r", func.__name__, result)
return result
return wrapper
@log_calls
def add(a, b):
return a + b
@log_calls
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
add(3, 4)
greet("Alice", greeting="Hi")
Decorators with Arguments
To pass arguments to a decorator you need one more level of nesting: a function that receives the arguments and returns the actual decorator:
from functools import wraps
def repeat(times):
"""Run the decorated function 'times' number of 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(3)
def say_hello(name):
print(f"Hello, {name}!")
say_hello("World")
# Another example: retry on exception
def retry(times=3, exceptions=(Exception,)):
"""Retry the function up to 'times' times if one of 'exceptions' is raised."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_error = e
print(f"Attempt {attempt}/{times} failed: {e}")
raise last_error
return wrapper
return decorator
import random
@retry(times=4, exceptions=(ConnectionError,))
def unstable_fetch():
if random.random() < 0.7:
raise ConnectionError("Network unavailable")
return "data"
Stacking Multiple Decorators
You can apply several decorators to a single function. They are applied bottom-up at decoration time, but execute top-down at call time:
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold # applied second (outer)
@italic # applied first (inner)
def greet(name):
return f"Hello, {name}"
# Equivalent to: greet = bold(italic(greet))
print(greet("Alice")) # <b><i>Hello, Alice</i></b>
Class Decorators
A class can be used as a decorator if it implements __init__ and __call__. This is useful when your decorator needs to maintain state:
from functools import wraps
class CallCounter:
"""Decorator that counts how many times a function has been called."""
def __init__(self, func):
wraps(func)(self) # copy metadata
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"[{self.func.__name__}] call #{self.count}")
return self.func(*args, **kwargs)
@CallCounter
def process(item):
return f"Processed: {item}"
process("apple")
process("banana")
process("cherry")
print(f"Total calls: {process.count}")
Built-in Decorators: @property, @staticmethod, @classmethod
Python ships with three widely-used built-in decorators for class design:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
# @property: access a method like an attribute (getter)
@property
def celsius(self):
return self._celsius
# @.setter: validate on assignment
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
# Computed property β no setter needed
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
# @classmethod: receives the class (cls) instead of the instance
@classmethod
def from_fahrenheit(cls, f):
return cls((f - 32) * 5/9)
# @staticmethod: no access to instance or class, just a utility
@staticmethod
def absolute_zero():
return -273.15
def __repr__(self):
return f"Temperature({self._celsius}Β°C)"
t = Temperature(100)
print(t.celsius) # 100
print(t.fahrenheit) # 212.0
t.celsius = 25
print(t.fahrenheit) # 77.0
body = Temperature.from_fahrenheit(98.6)
print(body) # Temperature(37.0Β°C)
print(Temperature.absolute_zero()) # -273.15
| Decorator | First parameter | Use case |
|---|---|---|
@property | self | Computed attributes, validation on set |
@classmethod | cls | Alternative constructors, factory methods |
@staticmethod | none | Utility functions that belong to the class namespace |
Caching with @functools.lru_cache and @functools.cache
Python's standard library ships with ready-made memoization decorators that can dramatically speed up recursive or expensive functions:
from functools import lru_cache, cache
import time
# Without caching β exponential time
def fib_slow(n):
if n < 2:
return n
return fib_slow(n - 1) + fib_slow(n - 2)
# With lru_cache β linear time, bounded cache
@lru_cache(maxsize=128)
def fib_fast(n):
if n < 2:
return n
return fib_fast(n - 1) + fib_fast(n - 2)
# @cache (Python 3.9+) β unbounded, simpler
@cache
def fib_cached(n):
if n < 2:
return n
return fib_cached(n - 1) + fib_cached(n - 2)
start = time.perf_counter()
print(fib_fast(40))
print(f"Cached: {time.perf_counter() - start:.6f}s")
# Inspect cache statistics
print(fib_fast.cache_info()) # CacheInfo(hits=38, misses=41, maxsize=128, currsize=41)
Real-World: Authentication Decorator
from functools import wraps
# Simulated user session
_current_user = {"username": "alice", "roles": ["editor"]}
def require_role(*roles):
"""Decorator factory: restrict access to users with one of the given roles."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
user_roles = _current_user.get("roles", [])
if not any(r in user_roles for r in roles):
raise PermissionError(
f"User '{_current_user['username']}' needs one of: {roles}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_database():
return "Database deleted!"
@require_role("editor", "admin")
def publish_article(title):
return f"'{title}' published."
try:
delete_database()
except PermissionError as e:
print(f"Access denied: {e}")
print(publish_article("Decorators in Python")) # Alice is an editor
ποΈ Practical Exercise
Write a @validate_types decorator that checks the types of all arguments against the function's type hints. If any argument doesn't match, raise a TypeError with a clear message. Test it on a function like def add(a: int, b: int) -> int.
Hint: use import inspect; inspect.signature(func).parameters and func.__annotations__.
π₯ Challenge Exercise
Build a @rate_limit(calls, period) decorator that allows at most calls invocations within any period-second window. If the limit is exceeded, raise a RateLimitError. Use a collections.deque to track call timestamps. Test it with @rate_limit(3, 10) β no more than 3 calls per 10 seconds.
Interview Questions
- What is a decorator in Python? What problem does it solve?
- What does the
@syntax actually do under the hood? - Why is
functools.wrapsimportant and what does it do? - How do you write a decorator that accepts its own arguments?
- What is the difference between a function decorator and a class decorator?
- When are decorators applied β at definition time or at call time?
- What is the difference between
@staticmethodand@classmethod? - What does
@propertydo, and why is it preferred over getters/setters? - How does
@lru_cachework? What does LRU stand for? - When would you use a class-based decorator instead of a function-based one?
π Summary
- Decorators are functions that wrap other functions to add behaviour without changing the original code.
@decoratoris syntactic sugar forfunc = decorator(func).- Always use
@functools.wrapsinside a decorator to preserve the wrapped function's metadata. - To pass arguments to a decorator, add one more level of nesting (a factory function).
- Multiple decorators stack bottom-up but execute top-down.
- Class-based decorators implement
__init__and__call__; they are ideal when state is needed. - Built-in decorators:
@property(computed attributes),@classmethod(factory methods),@staticmethod(utilities). @functools.lru_cacheand@functools.cacheprovide ready-made memoization.
Related Topics
Frequently Asked Questions
No. Decorators are applied once at definition time, when Python encounters the @ line. After that, every call goes through the wrapper function, but the decoration itself only runs once. This is why decorators are efficient for one-time setup like caching.
Not directly β once the function is replaced by the wrapper, the original is only accessible if you saved a reference to it. A common pattern is to expose the original via a wrapper.__wrapped__ attribute, which functools.wraps sets automatically.
Decorators add behaviour to individual functions or classes at definition time, without using inheritance. Mixins add behaviour to classes via multiple inheritance. Decorators are more composable for function-level concerns (timing, auth, caching); mixins are better for adding a family of methods to a class hierarchy.
Yes. Because the wrapper receives *args and **kwargs before passing them along, it can inspect, modify, filter, or augment them. This is how input-validation decorators and dependency-injection frameworks work.