⚡IntermediateFunctional Python

Decorators

Wrapping functions to add behaviour with the @decorator syntax logging, timing, caching, and more.

Overview

A decorator is a function that takes another function as input, wraps it with additional behaviour, and returns the wrapped function. The `@decorator` syntax is just shorthand for `fn = decorator(fn)`. Decorators are a direct application of Python's first-class functions since functions are objects, you can pass them as arguments, return them from other functions, and assign them to variables. A typical decorator defines an inner `wrapper` function that calls the original function and adds behaviour before/after (or both). The `functools.wraps` decorator should always be applied to the wrapper to preserve the original function's name, docstring, and signature without it, debugging and introspection tools see the wrapper instead of the real function. Common real-world uses: logging, timing/profiling, authentication checks, caching (`@functools.lru_cache`), retry logic, input validation, and rate limiting.

Key Points

  • •`@decorator` is syntactic sugar for `fn = decorator(fn)`
  • •A decorator is a function that takes a function and returns a (usually wrapped) function
  • •Always apply `@functools.wraps(fn)` to the wrapper to preserve metadata
  • •Decorators with arguments need an extra layer: `@retry(3)` → `decorator_factory(3)` returns a decorator
  • •`@functools.lru_cache(maxsize=128)` built-in memoisation decorator
  • •Multiple decorators stack bottom-up: `@a @b def f` → `f = a(b(f))`
  • •Class decorators work too apply to a class to modify or wrap it

Examples

Basic decorator with functools.wraps

import functools
import time

def timer(fn):
    @functools.wraps(fn)       # preserves fn.__name__, fn.__doc__
    def wrapper(*args, **kwargs):
        start = time.time()
        result = fn(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{fn.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

print(slow_sum(1_000_000))   # prints time, then the result
print(slow_sum.__name__)     # slow_sum (not wrapper, thanks to @wraps)

Decorator with arguments (factory pattern)

import functools

def repeat(times):           # outer function takes the argument
    def decorator(fn):       # middle function takes the function
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = fn(*args, **kwargs)
            return result
        return wrapper
    return decorator         # returns the actual decorator

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

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

Real-world examples: logging and lru_cache

import functools

def log_calls(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        print(f"Calling {fn.__name__}({args}, {kwargs})")
        result = fn(*args, **kwargs)
        print(f"{fn.__name__} returned {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 5)
# Calling add((3, 5), {})
# add returned 8

# Built-in memoisation decorator
@functools.lru_cache(maxsize=128)
def fib(n):
    if n < 2: return n
    return fib(n - 1) + fib(n - 2)

print(fib(50))   # fast, thanks to caching

Common Mistakes

  • Forgetting @functools.wraps your function loses its __name__ and __doc__, breaking help(), logging, and stack traces
  • Calling the function in the decorator body instead of returning the wrapper: `return wrapper` not `return wrapper()`
  • Decorators with arguments need three levels of nesting missing one level causes confusing TypeErrors

Note

Think of decorators as "plugins" for functions. They let you add cross-cutting concerns (logging, auth, timing) in one place without modifying every function that needs it. This is the Open/Closed principle open for extension, closed for modification.

Exercises

Write Python code to solve each exercise. Run it in your browser and check your output.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What is `@my_decorator` above a function definition equivalent to?