Decorators
Decorators are a powerful Python feature that lets you modify or
extend the behavior of functions (or classes) without permanently changing them.
They work by wrapping functions in other functions, adding functionality like
timing, logging, caching, or access control. The @ syntax makes
applying decorators clean and readable!
Understanding Decorators
At its core, a decorator is a function that takes another function as an argument and returns a modified (or wrapped) version of it. This allows you to add behavior before or after the original function executes.
Click Run to execute your code
1. A decorator is a function that accepts a function as an argument
2. It defines an inner wrapper function that adds extra behavior
3. The wrapper calls the original function and may add code before/after
4. The decorator returns the wrapper function
5. When you call the decorated function, you're actually calling the wrapper
The @ Syntax
Python provides the @decorator_name syntax as syntactic sugar for
applying decorators. Instead of manually wrapping functions, you can simply place
@decorator above the function definition!
Click Run to execute your code
@decorator syntax is equivalent to
function = decorator(function). It's applied at function definition
time, not when the function is called!
Preserving Function Metadata
By default, decorators can hide the original function's name, docstring, and
other metadata. Use functools.wraps to preserve this information,
which is important for debugging and documentation!
Click Run to execute your code
Decorators with Arguments
Sometimes you want decorators that accept arguments to customize their behavior. This requires an extra level of function nesting - a decorator factory that returns the actual decorator!
Click Run to execute your code
@decorator(args) not @decorator!
Multiple Decorators
You can apply multiple decorators to a single function. They're applied from bottom to top (the one closest to the function is applied first).
Click Run to execute your code
Common Mistakes
1. Forgetting parentheses when decorator needs arguments
# Wrong - missing parentheses
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat # Missing parentheses - will crash!
def greet():
print("Hello")
# Correct - use parentheses
@repeat(3)
def greet():
print("Hello")
2. Not preserving function metadata
# Wrong - loses original function info
def timer(func):
def wrapper(*args, **kwargs):
# ... timing code ...
return func(*args, **kwargs)
return wrapper
@timer
def calculate():
"""Performs complex calculation."""
pass
print(calculate.__name__) # 'wrapper', not 'calculate'!
print(calculate.__doc__) # None, not the docstring!
# Correct - use functools.wraps
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
# ... timing code ...
return func(*args, **kwargs)
return wrapper
3. Not accepting *args and **kwargs in wrapper
# Wrong - only works with no-argument functions
def logger(func):
def wrapper(): # No arguments!
print("Calling function")
return func()
return wrapper
@logger
def add(a, b): # Will crash when called with args!
return a + b
# Correct - accept any arguments
def logger(func):
def wrapper(*args, **kwargs): # Accept any arguments
print("Calling function")
return func(*args, **kwargs) # Pass them through
return wrapper
Exercise: Create a Timing Decorator
Task: Create a decorator that measures and prints the
execution time of a function. Use the time module and the
@ syntax.
Requirements:
- Import
timemodule - Create a
timingdecorator function - Use
functools.wrapsto preserve function metadata - Record start time before calling the function, end time after
- Print the elapsed time in seconds
- Apply the decorator to a test function
Click Run to execute your code
Show Solution
import time
from functools import wraps
def timing(func):
"""Decorator that measures function execution time."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
elapsed = end - start
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timing
def slow_function():
"""Simulates a slow operation."""
time.sleep(0.5) # Sleep for 0.5 seconds
return "Done"
@timing
def fast_function(n):
"""Performs quick calculation."""
return sum(range(n))
# Test the decorator
slow_function()
fast_function(1000000)
Summary
- Decorators: Functions that modify or extend other functions without changing them permanently
- @ Syntax:
@decoratoris shorthand forfunction = decorator(function) - Wrapper Pattern: Decorators wrap functions in wrapper functions that add behavior before/after execution
- functools.wraps: Preserves original function metadata (name, docstring) for debugging and documentation
- Decorator Arguments: Use decorator factories (functions returning decorators) when decorators need arguments
- Multiple Decorators: Applied bottom-to-top; closest to function is applied first
- Accept *args, **kwargs: Wrapper functions should accept any arguments and pass them to the original function
- Common Uses: Timing, logging, caching, access control, validation, retry logic
What's Next?
Decorators are powerful tools for extending function behavior! Next, we'll explore
context managers and the with statement, which
provide a clean way to manage resources and ensure proper cleanup. Context
managers work beautifully with decorators through the @contextmanager
decorator!
Enjoying these tutorials?