Web Analytics

Decorators

Advanced ~30 min read

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.

Output
Click Run to execute your code
How Decorators Work:
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!

Output
Click Run to execute your code
Pro Tip: The @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!

Output
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!

Output
Click Run to execute your code
Important: When using decorators with arguments, you need three levels: the decorator factory (accepts args), the decorator (accepts function), and the wrapper (calls the function). Don't forget the parentheses when calling: @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).

Output
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 time module
  • Create a timing decorator function
  • Use functools.wraps to 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
Output
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: @decorator is shorthand for function = 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!