By WebNest Studio Engineering Team
Python Tutorial
Python Decorators
A decorator is a function that takes another function (or class) as input and returns a modified or wrapped version of it, without permanently changing the original function's source code. Decorators are Python's way of cleanly separating "what a function does" from "extra behavior applied around every call to it," such as logging, timing, access control, or caching.
Decorators rely on the fact that functions are first-class objects in Python — they can be passed around, stored in variables, and returned from other functions just like any other value. The @decorator_name syntax placed above a function definition is simply shorthand for function = decorator_name(function).
Why it matters
Decorators are everywhere in Python frameworks: @app.get("/") in FastAPI, @pytest.fixture, @property, @login_required in Django. They add behaviour such as logging, timing, caching or access checks to a function without touching its code. You cannot read modern Python without understanding the @ line.
Writing a Basic Decorator
A decorator is typically written as a function that defines an inner "wrapper" function, calls the original function from inside that wrapper, and returns the wrapper. Because the wrapper usually needs to accept whatever arguments the original function accepts, it's conventional to define it as def wrapper(*args, **kwargs):, which forwards any positional and keyword arguments through to the wrapped function unchanged.
functools.wraps
Without extra care, wrapping a function replaces its __name__, __doc__, and other metadata with the wrapper function's own metadata — which makes debugging and introspection confusing, since help(my_function) would describe "wrapper" instead of the real function. The standard library's functools.wraps decorator, applied to the inner wrapper function, copies this metadata over from the original function automatically, and is considered a best practice for any decorator you write.
A Practical Example: Timing a Function
One of the most common real-world uses of a decorator is measuring how long a function takes to run, without modifying the function's own code at all — you record a start time before calling the wrapped function, call it, record an end time, and print or log the difference. The same pattern applies to logging (recording that a function was called and with what arguments) or to enforcing access control (checking a permission before allowing the wrapped function to run at all).
Examples
A timing decorator using functools.wraps
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} seconds")
return result
return wrapper
@timer
def slow_square(n):
total = 0
for i in range(n):
total += i * i
return total
result = slow_square(1000000)
print(result)
print(slow_square.__name__) # thanks to @wraps, this is "slow_square", not "wrapper"
slow_square took 0.0421 seconds
333332833333500000
slow_square
A logging decorator that reports arguments and return values
from functools import wraps
def log_calls(func):
@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}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 4)
Calling add with args=(3, 4), kwargs={}
add returned 7
Common Mistakes
- Forgetting *args and **kwargs in the wrapper function, which breaks the decorator for any function that takes arguments different from what was hardcoded.
- Omitting @functools.wraps, which silently replaces the wrapped function's __name__ and docstring, breaking introspection tools and confusing debugging output.
- Forgetting that the wrapper must actually call and return the result of the original function — otherwise the decorated function silently stops returning a value.
- Applying multiple decorators and misunderstanding their order — decorators closest to the function run first when the function is called, but are applied (wrapped) last when the file is loaded.
Exercise
Write a decorator shout that converts the text returned by a function to upper case. Use functools.wraps so that the decorated function keeps its original name.
Expected output: HELLO, ASHA then greet
from functools import wraps
def shout(function):
# TODO: define a wrapper that calls the function and returns the result in upper case
# TODO: return the wrapper
pass
@shout
def greet(name):
return f"Hello, {name}"
print(greet("asha"))
print(greet.__name__)
Show hints
- The wrapper should accept
*args, **kwargsand pass them on, so the decorator works with any function. - Put
@wraps(function)directly above the wrapper definition.
Show solution
from functools import wraps
def shout(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
return f"Hello, {name}"
print(greet("asha"))
print(greet.__name__)
Quick quiz
1. What is @timer above def work(): equivalent to?
- A. work = timer(work)
- B. timer.work()
- C. timer = work
- D. work(timer)
Show answer
A. The function is passed to the decorator and the name is rebound to what the decorator returns.
2. What does functools.wraps preserve?
- A. The speed of the function
- B. The name, docstring and other metadata of the original function
- C. The return value
- D. The arguments
Show answer
B. Without it, the decorated function reports the name of the wrapper.
3. When does the code of a decorator, outside its wrapper, run?
- A. Each time the decorated function is called
- B. Never
- C. Once, when the decorated function is defined
- D. When the program ends
Show answer
C. Only the wrapper runs on each call.
Interview questions
What is a decorator?
Show answer
A decorator is a function that takes a function and returns another function, usually a wrapper that runs some code before or after calling the original. The @name syntax applies it at the point of definition. It works because functions are objects that can be passed around and because the wrapper is a closure that remembers the original function.
How do you write a decorator that takes arguments, such as @retry(times=3)?
Show answer
Add one more level. The outer function receives the arguments and returns the actual decorator; the decorator receives the function and returns the wrapper. So retry(times=3) is called first, and its result is then applied to the function. That makes three nested functions: one for the arguments, one for the function, and the wrapper.
Key Points to Remember
- A decorator takes a function and returns a (usually wrapped) function; @decorator is shorthand for func = decorator(func).
- Wrapper functions should accept *args and **kwargs to transparently forward arguments to the original function.
- functools.wraps preserves the original function's __name__, __doc__, and other metadata on the wrapper.
- Common real-world uses include timing, logging, caching, and access control around existing functions.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs Python in your browser. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering