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_meis syntactic sugar forgreet = decorate_me(greet)Now, when you call
greet(), it will automatically go through thewrapperfunction 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_metakes a functionfuncas 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
wrapperis 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_meapplies 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 replacegreetwith the returnedwrapperfunction.”
Key Points:
The
@decoratormust 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:
*argscollects positional arguments (like"Sameer")**kwargscollects 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
*argsand**kwargsto 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
functoolsmodule:from functools import wrapsHelps keep the original function’s
__name__,__doc__, and other attributes intact.
When to Use:
Always use
@wrapswhen 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 |



