Skip to content
intermediate Phase 3 · Python Advanced

Decorators

Create and use function and class decorators for cross-cutting concerns.

1h 15m
0 problems
Topic Progress 0%

Decorator Basics

What is a Decorator?

A decorator is a function that takes a function and returns a new function. It's syntactic sugar for:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        # Code before function call
        result = func(*args, **kwargs)
        # Code after function call
        return result
    return wrapper

# Using decorator
@my_decorator
def say_hello(name):
    return f'Hello, {name}!'

# Equivalent to:
def say_hello(name):
    return f'Hello, {name}!'
say_hello = my_decorator(say_hello)

Simple Example

def timer_decorator(func):
    import time
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f'{func.__name__} took {end - start:.4f} seconds')
        return result
    return wrapper

@timer_decorator
def slow_function():
    import time
    time.sleep(1)
    return 'Done!'

slow_function()
# slow_function took 1.0012 seconds

Preserving Function Metadata

import functools

def my_decorator(func):
    @functools.wraps(func)  # Preserves func's metadata
    def wrapper(*args, **kwargs):
        """Wrapper documentation."""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """Greet someone."""
    return f'Hello, {name}!'

print(greet.__name__)  # greet (not 'wrapper')
print(greet.__doc__)   # Greet someone.

Decorators with Parameters

Three Levels of Functions

# Level 1: Regular decorator
def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# Level 2: Decorator factory (takes parameters)
def repeat(n):  # Takes decorator parameters
    def decorator(func):  # Takes function
        def wrapper(*args, **kwargs):  # Takes function arguments
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

# Level 3: Using the decorator
@repeat(3)  # repeat(3) returns decorator
def greet(name):
    print(f'Hello, {name}!')

greet('Alice')
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

Common Decorator Patterns

import functools
import time

# Retry decorator
def retry(max_attempts=3, delay=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
    import random
    if random.random() < 0.7:
        raise Exception('Random failure')
    return 'Success!'

# Cache decorator
@functools.lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(100))  # Fast! Otherwise would be very slow

Class-Based Decorators

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.call_count = 0
    
    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f'Call {self.call_count} to {self.func.__name__}')
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print('Hello!')

say_hello()  # Call 1 to say_hello
say_hello()  # Call 2 to say_hello
print(say_hello.call_count)  # 2

Common Decorator Patterns

Logging Decorator

import functools
import logging

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_str = ', '.join(repr(a) for a in args)
        kwargs_str = ', '.join(f'{k}={v!r}' for k, v in kwargs.items())
        all_args = ', '.join(filter(None, [args_str, kwargs_str]))
        logging.info(f'Calling {func.__name__}({all_args})')
        result = func(*args, **kwargs)
        logging.info(f'{func.__name__} returned {result!r}')
        return result
    return wrapper

Validation Decorator

import functools

def validate_types(*types):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for i, (arg, type_) in enumerate(zip(args, types)):
                if not isinstance(arg, type_):
                    raise TypeError(
                        f'Argument {i} must be {type_.__name__}, '
                        f'got {type(arg).__name__}'
                    )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(int, int)
def add(a, b):
    return a + b

add(1, 2)    # ✅ 3
# add(1, '2')  # ❌ TypeError

Memoization Decorator

import functools

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Or use built-in:
@functools.lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

Stacking Decorators

@decorator_a
@decorator_b
@decorator_c
def my_function():
    pass

# Equivalent to:
my_function = decorator_a(decorator_b(decorator_c(my_function)))

# Order matters!
@timer
@log_calls
def my_function():
    pass
# Calls log_calls first, then timer