Can you write this from memory?
Write a function `get_item()` (no params, body is `pass`) with the `@login_required` decorator applied
Python decorators look like magic until the syntax clicks:
@decabovedef f(...)is justf = dec(f)- Stacking matters:
@gabove@fmeansf = g(f(original))(bottom decorator applied first)
These exercises cover the patterns you'll actually use:
- Wrapper functions with
*args, **kwargs @functools.wraps(so tooling shows the original name/docs/signature)- Decorator factories (decorators with arguments)
- Practical built-ins like
@property,@classmethod,@staticmethod - Common stdlib decorators like
@lru_cachefor memoization
Here's the core idea:
@decorator
def func():
pass
is exactly equivalent to:
def func():
pass
func = decorator(func)
A decorator is just a function that takes a function and returns a (usually modified) function. That's it. This wrapping principle is an instance of the Decorator design pattern — a general OOP concept where you wrap an object to add behavior without modifying it.
When you stack decorators, the bottom one wraps first:
@auth_required
@log_calls
def delete_user(user_id):
...
Expands to:
delete_user = auth_required(log_calls(delete_user))
The inner decorator (log_calls) wraps the original function. The outer decorator (auth_required) wraps that result. When you call delete_user(), auth runs first, then logging, then the original.
A decorator that takes arguments needs an extra layer:
def factory(arg1, arg2): # 1. Factory captures arguments
def decorator(func): # 2. Decorator receives the function
@wraps(func)
def wrapper(*args, **kwargs): # 3. Wrapper does the work
# use arg1, arg2, func here
return func(*args, **kwargs)
return wrapper
return decorator
@factory("value1", "value2")
def my_func():
pass
@factory(...) is a function call that returns a decorator. That decorator then wraps my_func.
Without @wraps(func), your wrapper hides the original function's identity:
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def greet():
"""Say hello."""
pass
print(greet.__name__) # "wrapper" - wrong!
print(greet.__doc__) # None - lost the docstring!
With @wraps(func):
from functools import wraps
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def greet():
"""Say hello."""
pass
print(greet.__name__) # "greet" - correct!
print(greet.__doc__) # "Say hello." - preserved!
This matters for debugging, documentation tools, and inspect.signature(). For a copy-paste reference of decorator syntax patterns, see the Python decorators cheat sheet.
Use time.perf_counter() for measuring durations—it's high-resolution and monotonic (won't jump if the system clock changes):
from functools import wraps
from time import perf_counter
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return wrapper
The try/finally ensures timing is logged even if the function raises an exception.
When decorating a method, remember that self (or cls for classmethods) arrives as the first positional argument:
def log_method(func):
@wraps(func)
def wrapper(*args, **kwargs): # self is in args[0]
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Calculator:
@log_method
def add(self, a, b):
return a + b
If you wrote wrapper(a, b) instead of wrapper(*args, **kwargs), it would fail because it doesn't accept self.
A sync wrapper on an async function doesn't await—it just returns a coroutine object:
# BROKEN: sync wrapper on async function
def broken_timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = perf_counter()
result = func(*args, **kwargs) # Returns coroutine, doesn't run it!
print(f"Took {perf_counter() - start}s") # Lies: measures nothing
return result
return wrapper
For async functions, use async def and await:
def async_timer(func):
@wraps(func)
async def wrapper(*args, **kwargs):
start = perf_counter()
try:
return await func(*args, **kwargs) # Actually runs the coroutine
finally:
print(f"{func.__name__} took {perf_counter() - start:.4f}s")
return wrapper
@property - Turn a method into a read-only attribute:
@property
def full_name(self):
return f"{self.first} {self.last}"
@classmethod - Method receives the class, not an instance:
@classmethod
def from_json(cls, data):
return cls(**json.loads(data))
@staticmethod - No implicit first argument (just a namespaced function):
@staticmethod
def is_valid_email(email):
return "@" in email
@functools.lru_cache - Memoize expensive function calls:
@lru_cache(maxsize=128)
def expensive_computation(n):
...
@functools.cache (Python 3.9+) - Unbounded memoization:
@cache
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
Decorators can also wrap classes, not just functions. The pattern is the same: the decorator receives the class and returns a (possibly modified) class.
@dataclass - The most common class decorator, auto-generates boilerplate:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Equivalent to writing __init__, __repr__, __eq__ yourself
p = Point(1.0, 2.0)
print(p) # Point(x=1.0, y=2.0)
Custom class decorator - Register or modify classes:
registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class MyPlugin:
pass
# registry == {'MyPlugin': <class 'MyPlugin'>}
Class decorators are useful for ORMs, plugin systems, and reducing boilerplate. See the OOP guide for more on how @dataclass and class decorators fit into Python's object model.
When to Use Python Decorators: wraps, factories, stacking order & lru_cache
- Add cross-cutting behavior like logging, caching, retries, timing, or input validation without changing call sites.
- Enforce preconditions or permissions before executing a function.
- Wrap a function to add metrics/tracing while keeping the original API.
- Prefer stdlib decorators when they fit (e.g., @property, @classmethod, @lru_cache) before writing your own.
Check Your Understanding: Python Decorators: wraps, factories, stacking order & lru_cache
Write a decorator that times a function call.
Wrap the function, measure with time.perf_counter() (high-resolution), return the result, and use functools.wraps to preserve metadata.
What You'll Practice: Python Decorators: wraps, factories, stacking order & lru_cache
Common Python Decorators: wraps, factories, stacking order & lru_cache Pitfalls
- Forgetting @functools.wraps (tooling shows wrapper name/docs instead of the original)
- Decorator order bugs when stacking (g(f(x)) vs f(g(x)))
- Not handling *args/**kwargs so the wrapper breaks for different signatures
- Using time.time() for timing (prefer perf_counter for durations)
- Decorating async functions with a sync wrapper (you must await inside async def)
- Forgetting to return the wrapper (you return None and break the function)
- Methods: forgetting that wrapper receives self/cls via *args
Python Decorators: wraps, factories, stacking order & lru_cache FAQ
Why use functools.wraps?
It copies useful metadata from the wrapped function (like __name__ and __doc__) onto the wrapper, helping introspection tools and debuggers behave sensibly.
What does "stacking order" mean?
Decorators are applied bottom-to-top: @g above @f means the function becomes g(f(original)). The bottom decorator wraps first.
How do decorators with arguments work?
They are "decorator factories": an outer function captures parameters and returns the real decorator. Three layers: factory(args) → decorator(func) → wrapper(*args, **kwargs).
What's the difference between @classmethod and @staticmethod?
@classmethod receives cls as the first argument (can access class state); @staticmethod receives no implicit first argument (just a namespaced function).
Why does my decorated function show the wrong name?
You forgot @functools.wraps(func) in your wrapper. Without it, __name__, __doc__, and __signature__ reflect the wrapper, not the original function.
Can I decorate async functions?
Yes, but your wrapper must be async def and use await func(...). A sync wrapper just returns a coroutine object instead of executing the function.
What does functools.wraps do?
`@functools.wraps(func)` preserves the original function's metadata (name, docstring, module) when you wrap it with a decorator. Without it, the wrapper function replaces the original's identity, making debugging and introspection harder.
Python Decorators: wraps, factories, stacking order & lru_cache Syntax Quick Reference
@g
@f
def foo():
...
# Equivalent:
# foo = g(f(foo))from functools import wraps
from time import perf_counter
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__} took {perf_counter() - start:.4f}s")
return wrapperfrom functools import wraps
def repeat(n):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet():
print("Hello!")from functools import lru_cache
@lru_cache(maxsize=256)
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)from functools import wraps
def log_calls(func):
@wraps(func)
async def wrapper(*args, **kwargs):
print("calling", func.__name__)
return await func(*args, **kwargs)
return wrapperclass Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
@classmethod
def from_diameter(cls, d):
return cls(d / 2)
@staticmethod
def is_valid_radius(r):
return r > 0from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Generates __init__, __repr__, __eq__ automaticallyPython Decorators: wraps, factories, stacking order & lru_cache Sample Exercises
Write a decorator `log_call` that prints "Calling" before running any function. Use *args and **kwargs so it works with functions that take arguments.
def log_call(func):
def wrapper(*args, **kwargs):
print("Calling")
return func(*args, **kwargs)
return wrapperFill in the decorator that preserves the wrapped function's metadata. (Assume `import functools` is done)
@functools.wraps(func)What does this code print?
Decorating
Hello+ 6 more exercises
Copy-ready syntax examples for quick lookup