Learning Objectives
By the end of this lesson, you will understand how Python decorators work and how they can be used to extend the behavior of existing functions.
You will learn how to:
- Understand functions as first-class objects.
- Pass functions to other functions.
- Return functions from functions.
- Create basic decorators.
- Use the
@decoratorsyntax. - Handle function arguments with
*argsand**kwargs. - Preserve function metadata with
functools.wraps. - Create logging and timing decorators.
- Build authentication and access-control decorators.
- Use multiple decorators together.
- Create decorators that accept their own parameters.
What Are Decorators?
A decorator is a function that takes another function, adds or modifies behavior, and returns a new function.
In simple terms, a decorator allows you to say:
The original function does not need to be rewritten. The decorator wraps it with additional behavior.
def decorator(function):
def wrapper():
# additional behavior
function()
return wrapper
This pattern is extremely useful when the same additional behavior needs to be applied to many functions.
Why Decorators Matter
Imagine you have 20 functions and every function needs logging before it runs.
You could manually add logging code to every function, but that creates repetition.
def add(a, b):
print("Calling add")
return a + b
def subtract(a, b):
print("Calling subtract")
return a - b
def multiply(a, b):
print("Calling multiply")
return a * b
A decorator lets you separate the logging logic from the business logic.
Common uses of decorators
- Logging
- Performance measurement
- Authentication
- Authorization
- Caching
- Validation
- Retry logic
- Access control
- Monitoring
Functions Are Objects
To understand decorators, you first need to understand that Python functions are objects.
This means a function can be stored in a variable, passed to another function, and returned from a function.
def greet():
print("Hello!")
print(greet)
Python stores the function object in memory, and the
variable greet refers to that object.
Output
<function greet at 0x...>
Assigning Functions to Variables
Because functions are objects, another variable can refer to the same function.
def greet():
print("Hello, Python!")
message = greet
message()
Output
Hello, Python!
Both greet and message refer
to the same function.
Passing Functions as Arguments
A function can receive another function as an argument.
def greet():
return "Hello!"
def execute(function):
result = function()
print(result)
execute(greet)
Output
Hello!
Notice that we pass greet, not
greet().
greet refers to the function itself,
while greet() calls the function
immediately.
Returning Functions
Functions can also return other functions.
def outer():
def inner():
print("Inside inner function")
return inner
function = outer()
function()
Output
Inside inner function
This ability to create, pass, and return functions is the foundation of decorators.
Nested Functions
A nested function is a function defined inside another function.
def outer():
print("Outer function")
def inner():
print("Inner function")
inner()
outer()
Output
Outer function
Inner function
Decorators commonly use nested functions because the inner function becomes the wrapper around the original function.
Building a Basic Decorator
Let's create our first real decorator.
def my_decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper
def greet():
print("Hello!")
decorated = my_decorator(greet)
decorated()
Output
Before function
Hello!
After function
The decorator receives greet, creates a
wrapper, and returns that wrapper.
The @ Decorator Syntax
Python provides a cleaner syntax for applying decorators.
def my_decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper
@my_decorator
def greet():
print("Hello!")
greet()
This:
@my_decorator
def greet():
print("Hello!")
is conceptually equivalent to:
def greet():
print("Hello!")
greet = my_decorator(greet)
Decorators with Function Arguments
Real functions usually receive arguments. Therefore, our wrapper must be able to receive them too.
def logger(func):
def wrapper(*args, **kwargs):
print("Function:", func.__name__)
print("Arguments:", args)
print("Keyword arguments:", kwargs)
return func(*args, **kwargs)
return wrapper
@logger
def add(a, b):
return a + b
print(add(10, 20))
Output
Function: add
Arguments: (10, 20)
Keyword arguments: {}
30
Using *args and **kwargs
Decorators often need to work with functions that have different numbers and types of arguments.
That's why decorators commonly use:
*argsfor positional arguments**kwargsfor keyword arguments
def decorator(func):
def wrapper(*args, **kwargs):
print("Arguments received")
return func(*args, **kwargs)
return wrapper
This makes the decorator flexible enough to wrap many different functions.
Returning the Wrapped Function's Result
A common decorator mistake is forgetting to return the original function's result.
The correct pattern is:
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
You can also write:
return func(*args, **kwargs)
If the wrapped function returns a value and the
decorator does not return it, the caller may
receive None.
functools.wraps
When a decorator replaces a function with a wrapper, metadata such as the original function's name and documentation can be lost.
Python provides functools.wraps to solve
this problem.
from functools import wraps
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Calling:", func.__name__)
return func(*args, **kwargs)
return wrapper
Now Python can preserve important metadata from the original function.
Why use @wraps?
- Preserves the function name.
- Preserves the function docstring.
- Improves debugging.
- Improves introspection.
- Makes decorated functions behave more naturally.
Practical Decorator — Logging
Logging is one of the most common real-world uses of decorators.
from functools import wraps
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Function:", func.__name__)
print("Arguments:", args)
print("Keyword arguments:", kwargs)
return func(*args, **kwargs)
return wrapper
@logger
def calculate_total(price, quantity):
return price * quantity
print(calculate_total(500, 3))
Output
Function: calculate_total
Arguments: (500, 3)
Keyword arguments: {}
1500
The calculation itself remains clean while logging is handled separately.
Practical Decorator — Measuring Execution Time
Performance measurement is another excellent use case for decorators.
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(
f"{func.__name__} took "
f"{end - start:.6f} seconds"
)
return result
return wrapper
@timer
def process_data():
total = 0
for number in range(1_000_000):
total += number
return total
print(process_data())
The decorator measures execution time without changing
the main logic inside process_data().
time.perf_counter() is designed for
measuring elapsed time and is generally preferable
to ordinary wall-clock timestamps for performance
measurements.
Practical Decorator — Authentication
Decorators can also check whether a user is allowed to perform a particular operation.
from functools import wraps
def require_admin(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if user.get("role") != "admin":
print("Access denied.")
return None
return func(user, *args, **kwargs)
return wrapper
@require_admin
def delete_user(user, username):
print(f"Deleting {username}")
admin = {
"name": "Ali",
"role": "admin"
}
customer = {
"name": "Sara",
"role": "customer"
}
delete_user(admin, "Ahmed")
delete_user(customer, "Ahmed")
Output
Deleting Ahmed
Access denied.
In production applications, authentication and authorization should be implemented using the application's proper security framework. This example demonstrates the decorator pattern itself.
Using Multiple Decorators
A function can have more than one decorator.
@decorator_one
@decorator_two
def greet():
print("Hello!")
Decorators are applied from the bottom upward.
Conceptually, this is similar to:
greet = decorator_one(
decorator_two(greet)
)
Changing the order of decorators can change the behavior of the program.
Decorators with Their Own Parameters
Sometimes we want to configure a decorator.
For example, we might want a decorator that executes a function three times.
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet():
print("Hello!")
greet()
Output
Hello!
Hello!
Hello!
Notice that this requires three levels:
- The decorator configuration function.
- The actual decorator.
- The wrapper around the original function.
This pattern is commonly called a decorator factory.
Built-in Python Decorators
Python itself provides several useful decorators.
@staticmethod
class MathTools:
@staticmethod
def add(a, b):
return a + b
print(MathTools.add(10, 20))
A static method does not require an instance or
self.
@classmethod
class Product:
category = "Electronics"
@classmethod
def get_category(cls):
return cls.category
print(Product.get_category())
A class method receives the class as cls.
@property
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
circle = Circle(5)
print(circle.area)
The @property decorator allows a method to
be accessed like an attribute.
Common built-in decorators
@staticmethod@classmethod@property@functools.wraps
Common Decorator Mistakes
Mistake 1 — Forgetting to return the wrapper
def decorator(func):
def wrapper():
func()
# Missing return wrapper
The decorator must normally return the wrapper.
Mistake 2 — Forgetting function arguments
def decorator(func):
def wrapper():
return func()
return wrapper
This only works for functions that take no arguments.
A more flexible approach is:
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
Mistake 3 — Forgetting the return value
If the original function returns a value, the wrapper should normally return that value.
Mistake 4 — Not using functools.wraps
Without @wraps, useful metadata from the
original function can be lost.
Mistake 5 — Overusing decorators
Decorators are powerful, but too many layers can make code difficult to understand.
Use decorators when they make a repeated, cross-cutting behavior cleaner and more reusable.
Practice Exercises
Exercise 1 — Greeting Decorator
Create a decorator that prints
"Starting..." before a function and
"Finished!" after it.
Exercise 2 — Logging
Create a decorator that prints the function name whenever the function is called.
Exercise 3 — Calculator
Create a decorator that prints the arguments passed to a calculator function.
Exercise 4 — Timing
Build a decorator that measures how long a function takes to execute.
Exercise 5 — Role Checking
Create a decorator that allows a function to run
only when the supplied user has an
admin role.
Mini Challenge — Performance & Access Logger
Build a reusable decorator system for a small ecommerce application.
Your requirements
- Create a decorator that logs the function name.
- Log positional and keyword arguments.
- Measure execution time.
- Return the original function's result.
-
Preserve function metadata using
@wraps. - Create an optional role-checking decorator.
- Apply multiple decorators to an ecommerce function.
Start with a function such as:
def calculate_order_total(items):
total = 0
for item in items:
total += item["price"] * item["quantity"]
return total
Then create decorators such as:
@logger
@timer
def calculate_order_total(items):
total = 0
for item in items:
total += item["price"] * item["quantity"]
return total
Finally, experiment with the order of the decorators and observe how the execution flow changes.
Challenge Goal
Build a small reusable decorator toolkit that could realistically be used in a Python business application.
Knowledge Check
Test your understanding of Python decorators.
1. What is a decorator?
2. Which syntax applies a decorator?
3. Why are *args and **kwargs commonly used in decorators?
4. What does functools.wraps help preserve?
5. What happens when multiple decorators are stacked?
6. Which decorator is commonly used to create a computed property?
7. What should a wrapper usually do when the original function returns a value?
8. What does @staticmethod create?
9. What is a decorator factory?
10. Why are decorators useful in professional applications?
Lesson Summary
Decorators are one of Python's most powerful tools for creating reusable behavior around functions and methods.
Key Concepts
- Functions are first-class objects in Python.
- Functions can be passed as arguments.
- Functions can return other functions.
- Decorators wrap existing functions.
-
The
@decoratorsyntax provides a clean way to apply decorators. -
*argsand**kwargsmake decorators flexible. -
functools.wrapspreserves function metadata. - Decorators are useful for logging, timing, validation, authentication, and monitoring.
- Multiple decorators can be stacked.
- Decorator order can affect program behavior.
- Decorator factories allow decorators to receive configuration parameters.
The next lesson moves from decorators into another powerful Python tool: Regular Expressions. You will learn how to search, validate, extract, and transform text using patterns.
Finished this lesson?
Mark this lesson complete when you are comfortable creating and using decorators.