Python decorators are one of the language’s most powerful and expressive features. They allow you to extend and modify the behaviour of functions or classes without changing their actual code. This article will cover everything you need to know to understand, write, and use decorators effectively.
Table of Contents
What is a Decorator?
In Python, decorators are a powerful and flexible way to modify or extend the behaviour of functions or methods, without changing their actual code. A decorator is essentially a function that takes another function as an argument and returns a new function with enhanced functionality.
The Basics: Functions are first-class citizens
Python treats functions as first-class objects, meaning:
- They can be passed as arguments to other functions.
- They can be returned from other functions.
- They can be assigned to variables.
This enables decorators to work.
A Simple Example
def greet(name):
return f"Hello, {name}!"
say_hello = greet # Assigning function to a variable:
print(say_hello("Alice")) # Hello, Alice!
PythonWriting Your First Decorator
Here’s a simple decorator that prints something before and after a function runs:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function call")
result = func(*args, **kwargs)
print("After the function call")
return result
return wrapper
PythonUsing the decorator manually:
def say_hello():
print("Hello!")
decorated = my_decorator(say_hello)
decorated()
PythonUsing the @ syntax (Pythonic way):
@my_decorator
def say_hello():
print("Hello!")
say_hello()
'''
Output
Before the function call
Hello!
After the function call
'''PythonDecorators in the real world:
When I think of decorators in the real world, I think of a gun. What?? A Gun?! Before you think of shooting me, let me clarify. There are a lot of accessories you can use with a gun, like silencers, flashlights, laser scopes, stocks, pistol grips, etc.

Now think of a gun that is wrapped with a silencer. The silencer modifies the behaviour of the gun and performs some additional action, like suppressing the noise. The silencer extends the functionality of the gun without modifying it. It makes it easy to turn on and turn off the additional features.

Working of Decorator
- when you call say_hello(), You’re really calling wrapper().
- And inside the wrapper, the func (which is the original function say_hello) is called:
result = func(*args, **kwargs)Python- The return value of the original function is stored in result, which is then returned by wrapper.
Flow:
- You (the caller) call the function → Python calls the wrapper.
- wrapper calls the original function.
- The return value goes from the original function → to wrapper → back to you.
How is the wrapper called?
- wrapper is not called or executed inside my_decorator
- function wrapper is returned by my_decorator
- And then you call it outside using ()
decorated = my_decorator(say_hello)
decorated = wrapper #(because my_decorator returns wrapper)
decorated() -> wrapper() # my_decorator(say_hello)()
Python- my_decorator returns a wrapper
- () This is what is actually called a wrapper
What is a Closure?
A closure is a function object that captures and retains access to variables from its enclosing scope, even after that scope has finished execution.
A closure is formed when a nested function captures variables from its enclosing scope and retains access to them even after the outer function has completed execution.
def outer():
x = 10
def inner():
print(x)
return inner
f = outer() # inner
f() #outer()()PythonWhy two ()()
outer()() --> inner() #outer() -> innerPythonFlow
- outer() runs
- creates x = 10
- defines inner
- returns inner
- outer() is finished
- Normally, x should be gone…
- But when you call:f() –> 10
Why does this work?
Because:
- inner remembers x
- That memory = closure
A closure = function + its remembered environment
How does a wrapper remember the func?
Python stores func inside the closure (hidden memory) of the wrapper.
- func is NOT inside wrapper
- It comes from the outer scope
- So Python says: I need to capture this variable(closure)
Normally, func should be destroyed, But It is kept alive because the wrapper still needs it
Why do we use *args, **kwargs?
*args and **kwargs are just a way to capture whatever arguments the caller passes and forward them to the original function.
Wrapper should work for any function, i.e.
- no args
- positional args
- keyword args
Example
decorated(1, 2, x=3)
│
└── wrapper(1, 2, x=3)
├── args = (1, 2)
├── kwargs = {x: 3}
└── func(*args, **kwargs)
→ original function callPython*args and **kwargs allow decorators to handle arbitrary function signatures by forwarding all positional and keyword arguments to the wrapped function.
Example
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
def greet(name):
print(f"Hello, {name}!")
decorated = my_decorator(greet)
decorated("Backendmesh")
'''
Flow
decorated("Backendmesh")
→ wrapper("Backendmesh")
args = ("Backendmesh",)
kwargs = {}
→ func(*args, **kwargs)
→ greet("Backendmesh")
'''PythonSame example with a keyword argument
decorated(name="Backendmesh")
'''
Flow
wrapper(name="Backendmesh")
args = ()
kwargs = {"name": "Backendmesh"}
→ greet(name="Backendmesh")
'''PythonFunctions with Arguments
Positional argument
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before")
# modify first 3 positional arguments
new_args = (
args[0].upper(), # name -> uppercase
args[1] + 1, # age -> increment
args[2].lower() # city -> lowercase
)
result = func(*new_args, **kwargs)
print("After")
return result
return wrapper
def greet(name, age, city):
print(f"Hello, {name}! Age: {age}, City: {city}")
decorated = my_decorator(greet)
decorated("backendmesH", 25, "INDIA")
'''
Before
Hello, BACKENDMESH! Age: 26, City: india
After
'''PythonKeyword argument
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before")
kwargs["name"] = kwargs["name"].capitalize()
result = func(*args, **kwargs)
print("After")
return result
return wrapper
def say_hello(name):
print(f"Hello, {name}!")
decorated = my_decorator(say_hello)
decorated(name="backendmesh")
'''
Before
Hello, Backendmesh!
After
'''PythonSafety check
Because the function might be called without the ‘name’ variable
if "name" in kwargs:
kwargs["name"] = kwargs["name"].capitalize()
PythonNote: Any change to args or kwargs inside the decorator will reflect in the function as well.
Multiple decorators
@decorator_one
@decorator_two
def my_function():
print("Hello!")PythonThis is equivalent to:
my_function = decorator_one(decorator_two(my_function))PythonSo the decorator closest to the function is applied first, and then wrapped by the next one, and so on — like layers of an onion.
def decorator_one(func):
def wrapper_one(*args, **kwargs):
print("Decorator One - Before")
result = func(*args, **kwargs)
print("Decorator One - After")
return result
return wrapper_one
def decorator_two(func):
def wrapper_two(*args, **kwargs):
print("Decorator Two - Before")
result = func(*args, **kwargs)
print("Decorator Two - After")
return result
return wrapper_two
@decorator_one
@decorator_two
def say_hello():
print("Hello!")
say_hello()PythonOutput
Decorator One - Before
Decorator Two - Before
Hello!
Decorator Two - After
Decorator One - AfterPythonStep by step:
- decorator_two(say_hello) runs first → returns wrapper_two
- Then, decorator_one(wrapper_two) → returns wrapper_one
- So final, say_hello = wrapper_one
say_hello()
↓
wrapper_one()
↓
wrapper_two()
↓
original say_hello()PythonExecution order
- wrapper_one() starts
- prints Decorator One – Before
- Inside it calls: wrapper_two()
- wrapper_two() starts
- prints Decorator Two – Before
- Calls the original function, prints Hello!
- Back to wrapper_two()
- prints Decorator Two – After
- Back to wrapper_one()
- prints Decorator One – After
Note:
- wrapper_one() calls wrapper_two() : func inside wrapper_one refers to wrapper_two
- wrapper_two() calls original_function() : func inside wrapper_two refers to original_function
Summary
- say_hello() is first passed to decorator_two, which returns a new function (let’s call it wrapped_two)
- Then wrapped_two is passed to decorator_one, returning wrapped_one
- So calling say_hello() actually calls wrapped_one(), which calls wrapped_two(), and finally calls the original function.
Use Cases of Decorators
Decorators are great for many tasks:
- Logging
- Authentication / Authorization
- Memoisation / Caching (@lru_cache from functools)
- Performance timing
- Validation and sanitisation
- routing
Example: Logging
def log(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args} {kwargs}")
return func(*args, **kwargs)
return wrapperPythonBuilt-in Decorators
Python comes with several built-in decorators:
- @staticmethod
- @classmethod
- @property
- @functools.lru_cache
- @functools.wraps (used for preserving metadata when writing custom decorators)
Preserving Function Metadata with functools.wraps
When you write a decorator, the metadata (like name and docstring) of the original function gets lost. You can fix this with functools.wraps.
Sample Code
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Calling function...")
return func(*args, **kwargs)
return wrapper
PythonProblem without wraps
def decorator(func):
def wrapper():
print("Before")
func()
return wrapper
@decorator
def say_hello():
"""This is hello function"""
print("Hello")PythonCheck this
print(say_hello.__name__) #wrapper
print(say_hello.__doc__) #NonePythonYou lost the original info:
- function name
- docstring
Fix using functools.wraps
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper():
print("Before")
func()
return wrapper
@decorator
def say_hello():
"""This is hello function"""
print("Hello")PythonNow output
print(say_hello.__name__) #say_hello
print(say_hello.__doc__) #This is hello functionPythonWhat wraps actually does
Internally, it copies:
- __name__
- __doc__
- __module__
- __annotations__
- __dict__
From original function → wrapper
Why does this matter?
Without wraps:
- Debugging becomes confusing: Without functools.wraps, all decorated functions appear as wrapper in errors, logs, and debugging, so you lose the original function’s identity, and it becomes hard to trace issues.
- Logging shows wrong function names
- frameworks (Flask, FastAPI, pytest) may break
Decorators with Arguments
What if your decorator needs arguments?
def repeat(n):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Hello!")
say_hello()
PythonWhy need for one more function repeat?
- When you want your decorator to accept arguments (like @repeat(3)), you need an extra layer of function wrapping — a decorator factory.
- Python interprets @decorator_name as calling a function that takes just one argument — the function itself, so we need one more function
Class-Based Decorators
Decorators can also be implemented as classes by defining the __call__ method.
class MyDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Before")
result = self.func(*args, **kwargs)
print("After")
return result
@MyDecorator
def greet(name):
print(f"Hello, {name}!")
greet("Alice")PythonConclusion
Decorators are a powerful feature of Python, allowing you to enhance your functions or methods with minimal and readable syntax. Once you’re comfortable with higher-order functions and closures, decorators become a natural extension of your coding toolkit.