What are Decorators in Python?
Learn Python decorators: how they wrap functions, the @ syntax, functools.wraps, decorators with arguments, real use cases, and common interview questions.
Expected Interview Answer
A decorator is a callable that takes a function (or class) and returns a modified or wrapped version of it, letting you add behavior — logging, timing, caching, access control — around the original without changing its source code.
Because functions are first-class objects in Python, a decorator receives a function, defines an inner wrapper that runs extra logic before or after calling it, and returns that wrapper. The @decorator syntax above a definition is just sugar for func = decorator(func). Wrappers should accept *args and **kwargs to pass arguments through, and use functools.wraps to preserve the original function's name and docstring. Decorators can also take their own arguments via an extra enclosing function, and can be built as classes implementing __call__.
- Adds cross-cutting behavior without editing the wrapped function
- Promotes reuse of concerns like logging, timing, and caching
- Keeps business logic clean and separated from boilerplate
- Composable — multiple decorators can stack on one function
- Enables powerful library features (e.g. @property, @staticmethod, @lru_cache)
AI Mentor Explanation
Think of a third-umpire review wrapped around an on-field decision. The original call still happens, but before it stands, extra steps run — checking replays, logging the moment, confirming no-ball — and only then is the verdict returned. The batter's dismissal logic is untouched; the review layer simply adds checks around it, exactly as a decorator wraps a function.
Step-by-Step Explanation
Step 1
Treat functions as objects
Recall that functions can be passed as arguments and returned from other functions — the foundation of decorators.
Step 2
Write a decorator function
Define a function that takes func and defines an inner wrapper(*args, **kwargs) around it.
Step 3
Call through and augment
Inside wrapper, run logic before/after result = func(*args, **kwargs), then return result.
Step 4
Return the wrapper
The decorator returns wrapper; apply it with @decorator above the target function.
Step 5
Preserve metadata
Wrap the inner function with @functools.wraps(func) so __name__ and __doc__ survive.
Step 6
Parameterize if needed
For a decorator with arguments, add one more enclosing function that returns the actual decorator.
What Interviewer Expects
- Understanding of functions as first-class objects and closures
- Correct wrapper using *args and **kwargs
- Knowing @decorator is sugar for func = decorator(func)
- Using functools.wraps to preserve metadata
- Ability to write a decorator that takes arguments
- Real use cases: logging, timing, caching, access control
Common Mistakes
- Forgetting *args/**kwargs so the wrapper can't handle the real arguments
- Not returning the result of the wrapped function
- Omitting functools.wraps, losing the original __name__ and __doc__
- Confusing the extra layer needed for decorators that accept arguments
- Calling func() inside the decorator body instead of returning the wrapper
Best Answer (HR Friendly)
“A decorator in Python is a reusable wrapper you place on top of a function to add extra behavior — like logging or timing — without changing the function's own code. You write @name above a function, and Python runs your added logic around it every time it's called.”
Code Example
import functools
import time
def timed(func):
@functools.wraps(func) # preserve name and docstring
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timed # same as slow = timed(slow)
def slow(n):
return sum(range(n))
slow(1_000_000)import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}")
greet("Ada") # prints the greeting three timesFollow-up Questions
- Why is functools.wraps important when writing a decorator?
- How do you write a decorator that accepts its own arguments?
- How does @property work as a decorator?
- How do multiple stacked decorators evaluate — in what order?
- How would you implement a decorator as a class using __call__?
- What does @functools.lru_cache do and when would you use it?
MCQ Practice
1. The syntax @my_decorator above def f(): ... is equivalent to what?
@my_decorator is syntactic sugar for f = my_decorator(f) — the function is passed to the decorator and rebound to the result.
2. Why should a wrapper use *args and **kwargs?
*args and **kwargs let the wrapper accept and pass through whatever arguments the original function expects.
3. What does functools.wraps preserve on the wrapped function?
functools.wraps copies metadata like __name__ and __doc__ from the original function onto the wrapper.
Flash Cards
What is a decorator in one sentence? — A callable that takes a function and returns a wrapped version adding behavior around it, without changing its source.
What is @decorator shorthand for? — func = decorator(func) — it rebinds the name to the decorator's return value.
Why use functools.wraps? — It copies the original function's __name__, __doc__, and other metadata onto the wrapper so introspection still works.
How do you make a decorator that takes arguments? — Add an outer function that accepts the arguments and returns the actual decorator (three nested functions total).
Name three common decorator use cases. — Logging, timing, caching (lru_cache), access control, and retry logic.