Python Decorators
Learn how Python decorators work: writing your own, preserving metadata with functools.wraps, stacking decorators, and real-world use cases.
A decorator is a function that wraps another function to extend or modify its behavior without changing its source code. Decorators are one of Python's most powerful and idiomatic features — they are the engine behind @staticmethod, @classmethod, @property, @functools.lru_cache, and many popular web framework patterns.
This page covers how decorators work, how to write your own from scratch, how to pass arguments to decorators, how to stack them, and when each pattern is most useful.
How Decorators Work
A decorator is just a function that takes another function as its argument and returns a new function. Python provides the @ syntax as a shorthand for applying one:
@shout
def greet(name):
return f"hello, {name}"That is exactly equivalent to:
def greet(name):
return f"hello, {name}"
greet = shout(greet)The @shout line tells Python: after defining greet, immediately pass it to shout and rebind the name greet to whatever shout returns. From that point on, every call to greet(...) goes through shout's logic first.
Writing Your First Decorator
A decorator normally defines an inner wrapper function that calls the original function and adds extra behavior around it:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet("python")) # HELLO, PYTHONwrapper accepts *args and **kwargs so it forwards any combination of arguments to func unchanged. This makes the decorator compatible with any function regardless of its signature — a good habit from the start.
Why the Wrapper Must Return the Inner Function
shout ends with return wrapper, not return wrapper(). This is intentional: shout is building a new callable, not calling it yet. If you accidentally wrote return wrapper(), the decorator would run immediately at decoration time and greet would be bound to the return value of wrapper — a string — rather than to the callable itself.
Preserving Metadata with functools.wraps
Every Python function carries metadata: __name__, __doc__, __module__, and more. Without extra care, a decorator replaces the original function with wrapper, losing all of that:
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet.__name__) # wrapper — wrong
print(greet.__doc__) # None — lostFix this by applying @functools.wraps(func) to the wrapper. It copies the original function's metadata onto wrapper:
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
"""Say hello to name."""
return f"hello, {name}"
print(greet("world")) # HELLO, WORLD
print(greet.__name__) # greet
print(greet.__doc__) # Say hello to name.Always use @functools.wraps in any decorator you write. Without it, debugging tools, documentation generators, and test frameworks see the wrong function name. The only exception is when you intentionally want to hide the original identity.
Practical Decorator Examples
Logger
Log every call to a function with its arguments and return value:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8Timer
Measure how long a function takes to run:
import functools
import time
def timer(func):
@functools.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:.6f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
total = slow_sum(1_000_000)
print(total) # slow_sum took 0.01xxs then 499999500000time.perf_counter() is the right choice here because it has the highest available resolution for short-duration measurements.
Memoization (Cache)
Cache the return value for each unique set of arguments so the function is never computed twice for the same input:
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
print(fibonacci(30)) # 832040For production code, prefer the built-in @functools.lru_cache or @functools.cache (Python 3.9+), which handle edge cases, thread safety, and cache size limits. The hand-rolled version above is useful to understand the pattern.
Access Control
Guard a function so it can only run when a condition is met:
import functools
def require_auth(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_authenticated"):
raise PermissionError("Authentication required.")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def get_dashboard(user):
return f"Welcome, {user['name']}!"
guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}
try:
print(get_dashboard(guest))
except PermissionError as e:
print(e) # Authentication required.
print(get_dashboard(admin)) # Welcome, Admin!Decorators with Arguments
Sometimes you need to configure a decorator at decoration time — for example, to repeat a function a variable number of times. Plain decorators cannot take extra arguments directly because Python passes the function, not the arguments. The solution is a decorator factory: a function that accepts the configuration and returns a decorator:
import functools
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say(message):
print(message)
say("hello")
# hello
# hello
# helloReading from the outside in: @repeat(3) first calls repeat(3), which returns decorator. Python then applies decorator to say, which returns wrapper. So say ends up pointing at wrapper — same pattern as before, with the extra level just to carry n into scope.
The nesting can look daunting at first. A mental shorthand: the outermost function holds the configuration, the middle function holds the function being decorated, and the innermost function holds the call being intercepted.
Stacking Multiple Decorators
You can apply several decorators to a single function by stacking @ lines. Python applies them bottom-up — the decorator closest to the def is applied first:
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<b>" + func(*args, **kwargs) + "</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "<i>" + func(*args, **kwargs) + "</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # <b><i>Hello, Alice</i></b>Equivalent to greet = bold(italic(greet)). italic wraps greet first, then bold wraps the result. The output shows italic runs closer to the raw string and bold wraps the outside.
Class-Based Decorators
A class can be a decorator too — any object with a __call__ method is callable. Class-based decorators are useful when the decorator itself needs to maintain state across calls:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
print(say_hello.count) # 2functools.update_wrapper(self, func) does the same job as @functools.wraps — it copies the original function's metadata onto the instance. After decoration, say_hello is a CountCalls instance, so say_hello.count is a regular attribute access.
When to choose a class over a function decorator:
- You need persistent state (
count,cache, flags). - The decorator has multiple methods or helper logic.
- You need the decorated object to be introspectable as a specific type.
Decorator Gotchas
Forgetting to call the decorated function
A common early mistake is returning the wrapper but forgetting to call func inside it:
def broken(func):
def wrapper(*args, **kwargs):
print("before")
# forgot to call func!
return wrapperThe decorated function silently returns None every time. Always make sure wrapper calls func(*args, **kwargs) and returns its result.
Decorating at the wrong layer
When using parameterized decorators, forgetting the outer call is a frequent mistake:
# Wrong — 'repeat' receives the function, not a count
@repeat # should be @repeat(3)
def say(msg):
print(msg)This passes say to repeat where n is expected, causing a TypeError when say is called.
Decorator order matters
With stacked decorators the order changes behavior. @timer then @log_calls on the same function will time the already-logged version, while the reverse will log the already-timed version. Think through what you want each layer to see.
Relationship to Closures
A decorator's wrapper function is a closure — it captures func from the enclosing scope and keeps it alive even after the outer decorator function has returned. Understanding closures makes decorator internals obvious: the cell object holding func is exactly what lets wrapper call the original function long after shout(greet) completed.
For *args and **kwargs syntax used inside wrappers, see the dedicated chapter. For lambda expressions that pair well with decorators in higher-order patterns, see the lambda chapter.
Quick Reference
| Pattern | When to use it |
|---|---|
Basic wrapper | Add behavior before/after a function |
@functools.wraps | Always — preserves __name__, __doc__ |
| Decorator factory (3 levels) | Need to configure the decorator |
| Stacked decorators | Compose multiple independent behaviors |
| Class-based decorator | Need persistent state between calls |
@functools.lru_cache | Memoize pure functions (built-in, production-ready) |