A decorator is a function that allows you to wrap another function — to add or modify its behavior — without changing the original function’s code. This makes them perfect for enhancing functionality, enforcing rules, or adding features like logging or security checks.
I use decorators during code development to simulate different conditions, enforce constraints, logging, performance monitoring, and to validate outputs without cluttering the code.
Let’s demystify decorators by walking through realistic examples. Along the way, you’ll learn what decorators are, how they work, and why they’re a valuable tool in your Python programming toolkit.
Decorators are included in Python’s standard library. There are no external dependencies or pip
installs required.
Step 1: Setting Up Your Project
Start by creating a dedicated directory and a virtual environment for this project. Open your terminal and run:
Windows:
mkdir decorators_example
cd decorators_example
python -m venv venv
venv\Scripts\activate
Linux:
mkdir decorators_example
cd decorators_example
python3 -m venv venv
source venv/bin/activate
This setup ensures you’re working in an isolated environment, which keeps your dependencies organized.
Step 2: What Is a Decorator?
At its core, a decorator is a function that takes another function as input, wraps it with additional behavior, and then returns the modified function. This allows you to add new functionality to existing functions without changing their code.
Think of a decorator like a gift wrapper: the original gift (function) stays the same, but the presentation (decorated behavior) changes.
Let’s create a reusable decorator that adds logging to any function. While this example focuses on logging, the same principles apply to other use cases, such as authentication, caching, or performance monitoring.
Step 3: Writing the Decorator
Create a file named decorators.py
:
# decorators.py
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} completed")
return result
return wrapper
Explanation:
- The
logger
function takes a function (func
) as input. - It defines a nested
wrapper
function that adds behavior before and after calling the original function. - The
wrapper
function executes the original function and returns its result, but with extra steps (logging) wrapped around it.
Step 4: Applying the Decorator
Let’s use this decorator in a real-world scenario. Create a new file named store_operations.py
:
# store_operations.py
from decorators import logger
@logger
def process_order(order_id, customer_name):
print(f"Processing order {order_id} for {customer_name}")
return f"Order {order_id} processed"
@logger
def update_inventory(item_id, quantity):
print(f"Updating inventory for item {item_id}: {quantity} units")
return f"Inventory updated for item {item_id}"
if __name__ == "__main__":
process_order(123, "Alice")
update_inventory("SKU456", 50)
Explanation:
- The
@logger
syntax applies the decorator to bothprocess_order
andupdate_inventory
. - Every time one of these functions is called, the
logger
decorator adds logging behavior before and after the original function runs.
Step 5: Running the Code
Run the store_operations.py
file:
python store_operations.py
Output:
Calling function: process_order
Processing order 123 for Alice
Function process_order completed
Calling function: update_inventory
Updating inventory for item SKU456: 50 units
Function update_inventory completed
Even though you didn’t explicitly add logging code inside process_order
or update_inventory
, the decorator handled it for you. This shows how decorators reduce repetition and keep your functions focused on their core tasks.
Step 6: Why Use Decorators?
Reusability
Instead of writing the same logging logic in multiple functions, you encapsulate it in a single decorator. This makes your code more maintainable.
Clean Code
Decorators separate the core logic of a function from its additional behavior, like logging or validation. This keeps your functions simple and focused.
Flexibility
You can apply a decorator to any function. For example, if you add a new feature like generating invoices, you can use the same @logger
decorator without rewriting code.
Step 7: Going Beyond Logging
Decorators are not limited to logging. Let’s create a decorator that restricts when a function can run. Modify the decorators.py
file:
# decorators.py
from datetime import datetime
def time_restricted(start_hour, end_hour):
def decorator(func):
def wrapper(*args, **kwargs):
current_hour = datetime.now().hour
if start_hour <= current_hour < end_hour:
return func(*args, **kwargs)
else:
print("Function not allowed at this time.")
return wrapper
return decorator
This decorator checks the current time and only allows the wrapped function to run during a specified window.
Step 8: Using the Time-Restricted Decorator
Modify store_operations.py
:
from decorators import logger, time_restricted
@logger
@time_restricted(9, 17) # Only allow this function to run between 9 AM and 5 PM
def process_order(order_id, customer_name):
print(f"Processing order {order_id} for {customer_name}")
return f"Order {order_id} processed"
if __name__ == "__main__":
process_order(123, "Alice")
Run the script during and outside the allowed time range to see how the decorator works.
Step 9: Best Practices
- Keep Decorators Simple: A decorator should do one job well. Avoid overloading it with too many responsibilities.
- Use Built-In Decorators When Appropriate: Python provides decorators like
@staticmethod
,@classmethod
, and@functools.lru_cache
for common use cases. - Combine Decorators Carefully: When stacking decorators, the order in which they’re applied matters. Test thoroughly to ensure expected behavior.
Additional examples of decorators that serve different purposes. Each example builds on the concepts we’ve covered.
Example 1: Access Control Decorator
Decorator checks if a user has the correct permissions to execute a function. It’s useful in scenarios like web applications or API development where access control is essential.
Code Implementation
Create or update decorators.py
:
# decorators.py
def require_permission(required_permission):
def decorator(func):
def wrapper(user_permissions, *args, **kwargs):
if required_permission in user_permissions:
return func(*args, **kwargs)
else:
print(f"Access denied: missing permission '{required_permission}'")
return wrapper
return decorator
Usage Example
Update or create a file named access_control.py
:
# access_control.py
from decorators import require_permission
@require_permission("edit")
def edit_post(post_id):
print(f"Editing post {post_id}")
@require_permission("delete")
def delete_post(post_id):
print(f"Deleting post {post_id}")
if __name__ == "__main__":
user_permissions = ["view", "edit"] # Simulated user permissions
edit_post(user_permissions, 101) # Allowed
delete_post(user_permissions, 101) # Denied
Run the Code:
python access_control.py
Output:
Editing post 101
Access denied: missing permission 'delete'
Example 2: Retry Decorator
Decorator automatically retries a function if it fails, which is especially helpful in network calls or other operations prone to intermittent errors.
Code Implementation
Add the following to decorators.py
:
# decorators.py
import time
def retry(max_retries, delay):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(delay)
print("All attempts failed.")
return wrapper
return decorator
Usage Example
Create a file named retry_example.py
:
# retry_example.py
from decorators import retry
import random
@retry(max_retries=3, delay=2)
def unstable_operation():
print("Attempting operation...")
if random.random() < 0.7: # 70% chance to fail
raise ValueError("Operation failed.")
print("Operation succeeded!")
if __name__ == "__main__":
unstable_operation()
Run the Code:
python retry_example.py
Output (Example Output May Vary)
Attempting operation...
Attempt 1 failed: Operation failed.
Attempting operation...
Attempt 2 failed: Operation failed.
Attempting operation...
Operation succeeded!
Example 3: Execution Time Limiter
Decorator enforces a maximum runtime for a function. If the function exceeds the limit, it raises a timeout error.
Code Implementation
Add the following to decorators.py
:
# decorators.py
import signal
class TimeoutError(Exception):
pass
def time_limit(seconds):
def decorator(func):
def handle_timeout(signum, frame):
raise TimeoutError(f"Function '{func.__name__}' timed out after {seconds} seconds")
def wrapper(*args, **kwargs):
signal.signal(signal.SIGALRM, handle_timeout)
signal.alarm(seconds)
try:
return func(*args, **kwargs)
finally:
signal.alarm(0) # Disable the alarm
return wrapper
return decorator
Usage Example
Create a file named time_limiter.py
:
# time_limiter.py
from decorators import time_limit
import time
@time_limit(3) # Function must finish within 3 seconds
def long_running_task():
print("Starting long task...")
time.sleep(5) # Simulates a long task
print("Task completed.")
if __name__ == "__main__":
try:
long_running_task()
except TimeoutError as e:
print(e)
Run the Code:
python time_limiter.py
Output:
Starting long task...
Function 'long_running_task' timed out after 3 seconds
Thank you for reading this article. I hope you found it helpful and informative. If you have any questions, or if you would like to suggest new Python code examples or topics for future tutorials, please feel free to reach out. Your feedback and suggestions are always welcome!
Happy coding!
C. C. Python Programming
You can also find this tutorial on Medium.com