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

Example · python
import functools

# 1) Parameterised decorator: retry on failure
def retry(times, default=None):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except ValueError as e:
                    print(f'  attempt {attempt} failed: {e}')
            return default
        return wrapper
    return decorator

# 2) Class-based decorator that keeps state across calls
class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0
    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

attempts = iter([ValueError('nope'), ValueError('again'), 'success'])

@CountCalls
@retry(times=3, default='gave up')
def fetch():
    result = next(attempts)
    if isinstance(result, Exception):
        raise result
    return result

print('result   :', fetch())          # succeeds on 3rd internal attempt
print('call count:', fetch.count)     # 1 outer call
print('name kept :', fetch.__wrapped__.__name__)  # fetch

# Output:
#   attempt 1 failed: nope
#   attempt 2 failed: again
# result   : success
# call count: 1
# name kept : fetch

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.