Decorators, End to End

Wrap functions to add behaviour — plain decorators, decorators that take arguments, and class-based ones.

A decorator is a function that takes a function and returns a new function with extra behaviour wrapped around it. The @decorator syntax is just sugar: @log above def f means f = log(f).

The basic shape

import functools

def shout(func):
    @functools.wraps(func)          # keep func's name and docstring
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@shout
def greet(name):
    return f'hi {name}'

print(greet('ada'))   # HI ADA

Three levels you should know

  • Plain — @timer: wraps and returns.
  • Parameterised — @retry(times=3): a function that returns a decorator, so it needs one extra layer of nesting.
  • Class-based — a class with __call__, useful when the decorator needs to hold state (like a call counter).

Always apply functools.wraps to the wrapper — without it your decorated function loses its real name, docstring and signature, which breaks debugging and documentation tools.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • A web framework applies @route('/home') decorators to mark functions as URL handlers.
  • A @cache decorator wraps expensive computation functions so repeated calls return a stored result.
  • A class-based decorator tracks call counts and timings across all decorated functions in a service.

More examples

Decorator with functools.wraps

Wraps a function to log each call and its return value, preserving the original function's metadata.

Example · python
from functools import wraps

def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f'Calling {func.__name__}')
        result = func(*args, **kwargs)
        print(f'{func.__name__} returned {result!r}')
        return result
    return wrapper

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

add(3, 4)
# Calling add
# add returned 7

Parametrised decorator factory

A three-layer decorator factory that retries a function on specified exception types.

Example · python
from functools import wraps

def retry(times=3, exceptions=(Exception,)):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == times:
                        raise
                    print(f'Retry {attempt}: {e}')
        return wrapper
    return decorator

@retry(times=3, exceptions=(ValueError,))
def risky(): raise ValueError('oops')

try: risky()
except ValueError: print('All retries exhausted')

Class-based decorator

Implements a class-based decorator that counts how many times the wrapped function is called.

Example · python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

@CountCalls
def greet(name): print(f'Hello, {name}!')

greet('Alice')
greet('Bob')
print(greet.count)   # 2

Discussion

  • Be the first to comment on this lesson.