Home Blog About

Python Decorators Explained: From Basics to Advanced

Decorators are one of Python's most powerful features, yet many developers find them confusing at first. In this guide, I'll break down decorators step by step with practical examples you can use in your projects right away.

What Are Decorators?

At their core, decorators are functions that modify the behavior of other functions. They're a form of metaprogramming that allows you to add functionality to existing code without modifying it directly.

Your First Decorator

python
def my_decorator(func):
    def wrapper():
        print("Something before the function")
        func()
        print("Something after the function")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

# Call the decorated function
say_hello()

Output:

Something before the function
Hello!
Something after the function

Decorators with Arguments

Real functions often take arguments. Here's how to create a decorator that works with functions that accept any number of arguments.

Handling Function Arguments

python
import functools

def log_calls(func):
    @functools.wraps(func)  # Preserves function metadata
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    """Add two numbers."""
    return a + b

@log_calls
def greet(name, greeting="Hello"):
    """Greet someone."""
    return f"{greeting}, {name}!"

# Test the decorated functions
add(3, 5)
greet("Alice", greeting="Hi")

Output:

Calling add with args=(3, 5), kwargs={}
add returned 8
Calling greet with args=('Alice',), kwargs={'greeting': 'Hi'}
greet returned Hi, Alice!
Pro Tip: Always use @functools.wraps in your decorators. It preserves the original function's name, docstring, and other metadata.

Decorator Factory Pattern

Sometimes you need decorators that accept their own arguments. This is where the decorator factory pattern comes in.

Creating Configurable Decorators

python
import functools
import time

def retry(max_attempts=3, delay=1):
    """Decorator factory that retries a function on failure."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"Attempt {attempt} failed: {e}")
                    if attempt < max_attempts:
                        time.sleep(delay)
            
            raise last_exception
        return wrapper
    return decorator

# Usage with arguments
@retry(max_attempts=3, delay=0.5)
def fetch_data(url):
    # Simulated API call that might fail
    import random
    if random.random() < 0.7:  # 70% chance of failure
        raise ConnectionError("Network error")
    return {"data": "success"}

# The decorator will automatically retry on failure
result = fetch_data("https://api.example.com")

Practical Examples

Timing Decorator

Measure how long a function takes to execute—useful for performance optimization.

Execution Timer

python
import functools
import time

def timer(func):
    """Measure execution time of a function."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} executed in {end - start:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "Done"

slow_function()
# Output: slow_function executed in 1.0012 seconds

Memoization (Caching)

Cache function results to avoid redundant computations. Python has a built-in solution, but understanding how to build one is valuable.

Simple Cache Implementation

python
import functools

def memoize(func):
    """Cache function results for repeated calls."""
    cache = {}
    
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    
    wrapper.cache = cache  # Expose cache for debugging
    return wrapper

@memoize
def fibonacci(n):
    """Calculate nth Fibonacci number (recursive)."""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Now it's fast even for large numbers
print(fibonacci(100))  # 354224848179261915075

# Or just use Python's built-in:
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci_builtin(n):
    if n < 2:
        return n
    return fibonacci_builtin(n - 1) + fibonacci_builtin(n - 2)

Class Decorators

Decorators can also be applied to classes, and you can use classes as decorators.

Class-based Decorator

python
class CountCalls:
    """Decorator class that counts function calls."""
    
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call {self.count} of {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hi():
    print("Hi!")

say_hi()  # Call 1 of say_hi
say_hi()  # Call 2 of say_hi
say_hi()  # Call 3 of say_hi

print(f"Total calls: {say_hi.count}")  # Total calls: 3

Key Takeaways

  • Decorators wrap functions to add behavior without modifying the original code
  • Use *args and **kwargs to handle any function signature
  • Always use @functools.wraps to preserve function metadata
  • Decorator factories let you create decorators that accept arguments
  • Python's @lru_cache is a powerful built-in for memoization

Decorators are used extensively in popular frameworks like Flask (@app.route), Django (@login_required), and pytest (@pytest.fixture). Mastering them will make you more effective at reading and writing Pythonic code.

Keyur Bhatiya

Keyur Bhatiya

Python developer and AI enthusiast. I write about backend development, machine learning, and building production-ready applications.