Home/Learn/Python A–Z/Decorators — Wrapping Functions with Functions

Decorators — Wrapping Functions with Functions

Advanced
Iterators & Decorators

@decorator replaces a function with a wrapped version — the mechanism behind logging, timing, caching, auth checks, and every @app.get you'll write in FastAPI.

Overview

A decorator is a function that takes a function and returns a replacement — @deco above def f is exactly f = deco(f). Combined with closures (the wrapper remembers the original) and *args/**kwargs (the wrapper forwards anything), decorators add behaviour around functions without touching their code. functools.wraps preserves the original's name/docstring — skip it and debugging tools lie to you. Parameterized decorators (@retry(times=3)) add one more nesting level: a factory that returns a decorator.

The Pattern, Step by Step

wrapper(*args, **kwargs) accepts anything, adds behaviour, and forwards the call. @wraps(func) copies metadata. This template is 90% of every decorator ever written.

The universal decorator template
import time
from functools import wraps

def timed(func):
    @wraps(func)                          # keep func's name/docs
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)    # call the original
        ms = (time.perf_counter() - start) * 1000
        print(f"{func.__name__} took {ms:.1f} ms")
        return result
    return wrapper

@timed                                    # slow_sum = timed(slow_sum)
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)      # slow_sum took ~250 ms
print(slow_sum.__name__)  # 'slow_sum' — thanks to @wraps
                          # without it: 'wrapper' (breaks debugging)

Decorators with Arguments & Real Uses

@retry(times=3) calls retry(3) FIRST, which returns the actual decorator — three levels: factory → decorator → wrapper. This is the shape used by @app.get("/path") in FastAPI.

Parameterized decorators — the FastAPI shape
from functools import wraps
import time

def retry(times=3, delay=0.1):            # 1) factory takes config
    def decorator(func):                  # 2) decorator takes func
        @wraps(func)
        def wrapper(*args, **kwargs):     # 3) wrapper runs it
            last = None
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last = e
                    print(f"attempt {attempt} failed: {e}")
                    time.sleep(delay)
            raise last
        return wrapper
    return decorator

@retry(times=3)
def flaky_api():
    import random
    if random.random() < 0.7:
        raise ConnectionError("timeout")
    return "data"

# Stacking — applied bottom-up: timed(retry(...)(f))
# @timed
# @retry(times=2)
# def fetch(): ...

# You already use decorators daily:
# @property, @staticmethod, @dataclass, @lru_cache, @app.get("/users")

Key Points to Remember

  • 1@deco is exactly f = deco(f) — a function replaced by its wrapped version
  • 2Always @wraps(func) the wrapper — preserves __name__/__doc__ for debugging
  • 3Parameterized decorators are factories: retry(3) returns the decorator
  • 4Stacked decorators apply bottom-up

Interview Questions

Sign in to ask Aria
1

Write a decorator that logs arguments and return value of any function.

MediumThoughtworks
2

What breaks if you forget functools.wraps?

MediumCRED
3

How does @retry(times=3) work — walk through all three nesting levels.

HardUber

Ask Aria about Decorators — Wrapping Functions with Functions

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…