Decorators
Learn how decorators let you wrap any function in extra behavior — like logging or timing — without touching the original code.
Imagine you run a small cafe. Every barista has their own way of making coffee, but you want to time every drink and log it to a notebook — without retraining anyone or rewriting their recipes.
That is exactly what a decorator does in Python. It wraps a function — any function — and quietly adds behavior around it. The original function never changes. The decorator just slips a new layer around the outside, like gift wrap.
See it in action
— step through the idea, then dive into the details below.Gift-Wrap Any Function
A decorator adds behavior to a function — logging, timing, validation — without changing a single line of the original. It's a reusable wrapper you snap on with one line.
#Functions Are First-Class Objects
Before decorators make sense, you need to know one surprising thing: in Python, functions are values, just like numbers or strings. You can store a function in a variable, pass it to another function, or return it from a function. This is called being a first-class object.
def say_hello():
print("Hello!")
# Store a function in a variable
greeter = say_hello
# Call it through the variable
greeter()def run_twice(func):
func()
func()
def clap():
print("*clap*")
run_twice(clap)Functions as gift boxes
Think of a function as a gift box with instructions inside. You can hand that box to someone (pass it), put it on a shelf (store it in a variable), or even put it inside another box (return it from a function). The instructions do not run until someone opens the box — i.e., calls it with ().
#Returning a Function From a Function
Here is the other key idea: a function can create and return another function. The returned function is called a closure — it remembers the environment in which it was created.
def make_multiplier(n):
def multiply(x):
return x * n # 'n' is remembered from the outer function
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))
print(triple(5))#Building Your First Decorator
A decorator is a function that:
- Takes a function as its argument
- Defines a wrapper function inside itself that adds behavior
- Returns the wrapper
The wrapper calls the original function, but can do things before and after.
def shout_decorator(func):
def wrapper():
print(">>> Getting ready...")
func() # call the original
print("<<< Done!")
return wrapper
def greet():
print("Hi there!")
# Wrap greet manually
greet = shout_decorator(greet)
greet()#The @ Syntax — Decorator Sugar
Writing func = decorator(func) every time is repetitive. Python gives you the @ shorthand — place it right above the function definition and Python does the wrapping automatically. These two snippets do exactly the same thing.
def shout_decorator(func):
def wrapper():
print(">>> Getting ready...")
func()
print("<<< Done!")
return wrapper
@shout_decorator # same as: greet = shout_decorator(greet)
def greet():
print("Hi there!")
greet()#Handling Any Function with *args and **kwargs
The shout_decorator above only works on functions with no arguments. What about functions that take parameters?
The trick is to make your wrapper accept *args and **kwargs and forward them straight through. This way the decorator works on any function, no matter its signature.
import time
def timer(func):
def wrapper(*args, **kwargs): # accept anything
start = time.time()
result = func(*args, **kwargs) # forward everything
end = time.time()
elapsed = end - start
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result # don't swallow the return value!
return wrapper
@timer
def add(a, b):
return a + b
total = add(10, 20)
print("Result:", total)Always return the result!
If your wrapper calls func(*args, **kwargs) but forgets to return the result, the decorated function will silently return None. Always capture the return value in a variable and return it from the wrapper.
#Preserving Metadata with functools.wraps
When you wrap a function, Python can get confused about the function's name and documentation. The wrapper replaces the original, so func.__name__ and func.__doc__ point to the wrapper instead of the real function. The fix is one line: @functools.wraps(func) applied to your wrapper.
import functools
def timer(func):
@functools.wraps(func) # copies name, docstring, and more
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def multiply(a, b):
"""Multiply two numbers."""
return a * b
print(multiply.__name__) # would be 'wrapper' without functools.wraps
print(multiply.__doc__)Always add @functools.wraps
Make it a habit: every time you write a decorator, add @functools.wraps(func) to the inner wrapper. It costs nothing and saves you from mysterious debugging sessions where a function claims to be named wrapper.
#A Practical Logging Decorator
Let's put everything together in a decorator you might actually use — one that logs what function was called, with what arguments, and what it returned.
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"Calling {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def power(base, exponent=2):
return base ** exponent
power(3)
power(2, exponent=10)#Where Decorators Appear in the Wild
You will meet decorators everywhere once you start reading Python code:
- `@staticmethod` / `@classmethod` — Python built-ins that change how a method works in a class.
- `@property` — turns a method into an attribute-style access.
- Flask / Django routes —
@app.route("/home")registers a URL handler with zero boilerplate. - `@pytest.fixture` — marks a function as test setup in the pytest framework.
- Caching —
@functools.lru_cachecaches expensive results automatically.
Each of these is just a decorator following the same pattern you just learned.
import functools
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(35))What does the line `@my_decorator` above a function definition actually do?
Key takeaways
- In Python, functions are **first-class objects** — they can be stored in variables, passed to functions, and returned from functions.
- A decorator is a function that takes a function and returns a **new function** with added behavior wrapped around the original.
- The `@decorator` syntax is just shorthand for `func = decorator(func)` — they are identical.
- Use `*args` and `**kwargs` in your wrapper so your decorator works on **any function**, regardless of its parameters.
- Always add `@functools.wraps(func)` to your inner wrapper to preserve the original function's name, docstring, and other metadata.
What does this code print?
def shout(func):
def wrapper():
print(">>>")
func()
print("<<<")
return wrapper
@shout
def greet():
print("Hello!")
greet()This decorator is supposed to print the result of calling add(3, 4), but it always prints None instead. What is wrong?
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
return a + b
print(add(3, 4))Complete the decorator so it prints 'Starting...' before the wrapped function runs, and works on functions with any number of arguments.
import functools def announce(func): @functools.wraps(func) def wrapper(): print("Starting...") return func() return wrapper @announce def add(a, b): return a + b print(add(2, 3))
Put these lines in the right order to define a working decorator and apply it to a function.
def bold(func):
return wrapper
def wrapper():
print("**")func()
print("**", end="")Write a decorator called require_positive that checks every argument passed to a function. If any argument is zero or negative, it should print an error message and return None instead of calling the function. If all arguments are positive, call the function normally and return its result. Test it on a divide(a, b) function.
Try it live — edit the code and hit Run to execute real Python: