Understanding Python Decorators: A Practical Guide


Decorators in Python might look a bit strange at first (especially that @
symbol), but they’re actually a powerful and elegant way to modify the behavior of functions — without changing their actual code.
This guide will walk you through decorators step by step, using simple, real-world examples and clean code you can understand and use right away.
📌 What is a Decorator?
In Python, a decorator is a function that takes another function as input and returns a modified version of that function.
In short:
You wrap a function with extra functionality.
You don’t modify the function’s actual body.
You use the
@
syntax to apply it.
Let’s break that down with an example.
A Simple Example – Without Using @
def decorate_me(func):
def wrapper():
print("Before the function runs")
func()
print("After the function runs")
return wrapper
def greet():
print("Hello!")
decorated_greet = decorate_me(greet)
decorated_greet()
Output:
Before the function runs
Hello!
✅ After the function runs
What’s happening:
decorate_me()
is a decorator.It returns
wrapper()
, which runs some extra code before and after calling the original functiongreet()
.greet()
remains unchanged, but you can run it with extra logic around it.
Using the @decorator
Syntax (Syntactic Sugar)
Python offers a cleaner way to apply decorators using the @
symbol:
You can rewrite the given code using the @
decorator syntax like this:
def decorate_me(func):
def wrapper():
print("Before the function runs")
func()
print("After the function runs")
return wrapper
@decorate_me
def greet():
print("Hello!")
greet()
Explanation:
@decorate_me
is syntactic sugar forgreet = decorate_me(greet)
Now, when you call
greet()
, it will automatically go through thewrapper
function defined in the decorator.
Code:
def decorate_me(func):
def wrapper():
print("Before the function runs")
func()
print("After the function runs")
return wrapper
🔹 What is this?
This is a decorator function.
decorate_me
takes a functionfunc
as an argument.Inside it, another function
wrapper()
is defined. This:Prints a message before calling
func()
Calls the original function
func()
Prints a message after the function runs
Then
wrapper
is returned — not called yet, just returned as a function.
Now the decorated function:
@decorate_me
def greet():
print("Hello!")
This:
@greet = decorate_me(greet)
So now greet()
is actually pointing to the wrapper()
function returned by decorate_me
.
Final call:
greet()
This now calls the wrapper()
function, which behaves like:
print("Before the function runs")
greet() # original one: prints "Hello!"
print("After the function runs")
Final Output:
Before the function runs
Hello!
After the function runs
🎯 Summary:
The
@decorate_me
applies extra logic before and after running the original function.Useful for logging, validation, authentication, timing, etc., without changing the main function code.
“Take
greet
, pass it todecorate_me()
, and replacegreet
with the returnedwrapper
function.”
Key Points:
The
@decorator
must be placed directly above the function definition.You can stack multiple decorators on a single function by using multiple
@
lines.Decorators help in adding reusable features like logging, timing, validation, or authentication without modifying the core function.
When to Use:
To apply the same piece of logic (like logging or checking permissions) to many functions.
To make your code cleaner, more modular, and easier to manage.
Common Use Cases:
Logging function calls
Measuring execution time
Access control and authentication
Input validation
Caching results
Why Are Functions "First-Class" in Python?
In Python, the term first-class refers to first-class objects (or citizens) — and functions in Python are first-class objects.
✅ What Does “First-Class” Mean?
It means that a value (like a function) can be:
Assigned to a variable
Passed as an argument to another function
Returned from another function
Stored in data structures (like lists, dicts)
🔹 Example: Functions Are First-Class in Python
def greet(name):
return f"Hello, {name}!"
# 1. Assigned to a variable
say_hello = greet
print(say_hello("Alice")) # Output: Hello, Alice!
# 2. Passed as argument
def call_function(func, value):
return func(value)
print(call_function(greet, "Bob")) # Output: Hello, Bob!
# 3. Returned from another function
def get_greeter():
return greet
new_func = get_greeter()
print(new_func("Charlie")) # Output: Hello, Charlie!
Explanation:
Part 1: Define a simple function
def greet(name):
return f"Hello, {name}!"
This defines a function greet
that takes a name
and returns a greeting string.
Example:
greet("Alice") ➝ "Hello, Alice!"
✅ Step 1: Assigned to a variable
say_hello = greet
Here, you're not calling the function with ()
— you're assigning the function itself to a new variable say_hello
.
So now:
say_hello("Alice") ➝ "Hello, Alice!" # behaves exactly like greet()
Print Output:
Hello, Alice!
This shows that the function can be assigned to a variable — a feature of first-class objects.
✅ Step 2: Passed as an argument to another function
def call_function(func, value):
return func(value)
This defines a new function call_function
, which accepts:
func
: a functionvalue
: a value to pass into that function
Now:
print(call_function(greet, "Bob"))
➡ greet
is passed as an argument
➡ call_function
calls it: greet("Bob")
➡ Output: "Hello, Bob!"
Print Output:
Hello, Bob!
✅ Step 3: Returned from another function
def get_greeter():
return greet
This function returns the greet function itself, not a value.
When you call:
new_func = get_greeter()
Now new_func
is equal to the original greet
function.
So:
new_func("Charlie") ➝ "Hello, Charlie!"
Print Output:
Hello, Charlie!
Summary Table:
Operation | Result |
Assign function to variable | say_hello = greet |
Pass function as an argument | call_function(greet, val) |
Return function from another one | get_greeter() ➝ greet |
Conclusion:
This code shows functions are first-class citizens in Python — they can be used like data:
assigned
passed
returned
Decorators with Parameters Using *args
and **kwargs
Great! Let's dive into decorators with parameters using *args
and **kwargs
.
🧠 Why Use *args
and **kwargs
?
When writing decorators, you might want to decorate functions that:
Have any number of arguments (like
func(a, b)
,func(name="Sameer")
, etc.)To handle this flexibly, decorators use
*args
(positional) and**kwargs
(keyword arguments).
✅ Basic Example
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function runs")
result = func(*args, **kwargs)
print("After the function runs")
return result
return wrapper
@my_decorator
def greet(name):
print(f"Hello, {name}!")
greet("Sameer")
🔍 Explanation:
*args
collects positional arguments (like"Sameer"
)**kwargs
collects keyword arguments (likename="Sameer"
)The decorator passes those along to the actual function.
🧪 Output:
Before the function runs
Hello, Sameer!
After the function runs
Another Example: Function with multiple args
def debug_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
return wrapper
@debug_decorator
def add(x, y):
return x + y
@debug_decorator
def greet(name="Guest"):
print(f"Hi, {name}!")
print(add(10, 5))
greet(name="Sameer")
Output:
Calling add with args=(10, 5), kwargs={}
15
Calling greet with args=(), kwargs={'name': 'Sameer'}
Hi, Sameer!
🎯 Summary:
Use
*args
and**kwargs
to make your decorator work with any kind of function.This makes your decorators generic and reusable.
Real-World Decorator Examples
Let’s explore some real, useful use-cases for decorators in actual applications.
Logging Function Calls
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}...")
return func(*args, **kwargs)
return wrapper
@log_call
def download_file(filename):
print(f"Downloading {filename}...")
download_file("report.pdf")
Use Case: Useful for debugging or tracking what’s being called.
Timing How Long a Function Takes
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} ran in {end - start:.2f} seconds.")
return result
return wrapper
@timer
def slow_operation():
time.sleep(1)
print("Done.")
slow_operation()
Use Case: Benchmark or optimize functions.
Check User Authentication
def require_auth(func):
def wrapper(*args, **kwargs):
if kwargs.get("is_authenticated"):
return func(*args, **kwargs)
else:
print("🚫 Access Denied. Please log in.")
return wrapper
@require_auth
def view_dashboard(user, is_authenticated=False):
print(f"Welcome, {user}!")
view_dashboard("Sameer", is_authenticated=True)
view_dashboard("Guest", is_authenticated=False)
Use Case: Security checks in web apps or APIs.
Why Use functools.wraps
?
When you decorate a function, you lose its name and docstring, because you're replacing it with the wrapper.
📌 functools.wraps
in Python
In Python, functools.wraps
is a decorator used inside custom decorators to preserve the original function’s metadata — such as its name, docstring, and annotations.
What It Does:
When you create a decorator, it wraps another function inside a new one. Without functools.wraps
, the wrapped function loses its identity.
This can affect:
Debugging
Introspection (
__name__
,__doc__
, etc.)Tools that rely on metadata (like documentation generators or test frameworks)
To preserve the original function’s identity, use functools.wraps
:
from functools import wraps
def log(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Running {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log
def greet():
"""Greets the user"""
print("Hi!")
print(greet.__name__) # greet
print(greet.__doc__) # Greets the user
Without @wraps
, greet.__name__
would return 'wrapper'
, which is confusing.
Key Points:
Use
@wraps(func)
inside your decorator's wrapper function.Comes from the
functools
module:from functools import wraps
Helps keep the original function’s
__name__
,__doc__
, and other attributes intact.
When to Use:
Always use
@wraps
when creating custom decorators.Especially important in production code, debugging, and tools that depend on function metadata
When Should You Use Decorators?
Use decorators when you want to:
Avoid repeating common logic (DRY principle)
Add logging, validation, security, or performance tracking
Wrap third-party functions cleanly
When Not to Use Decorators?
Avoid decorators when:
You’re working on very simple logic
You need custom logic that can't be easily abstracted
It adds confusion rather than clarity
Summary Table
Feature | Description |
@decorator | Syntactic sugar for applying a decorator function |
*args , **kwargs | Allow decorators to accept any function signature |
functools.wraps | Preserves original function name and docstring |
Use Cases | Logging, timing, access control, validation, caching |
Subscribe to my newsletter
Read articles from Shaik Sameer directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
