Skip to content
UniCircle
MediumConceptualDecorators and closures

How do decorators work? Write a timing decorator

Question PY-DEC-006

Explain what @decorator above a function does. Write a decorator that prints how long a function took, and explain why functools.wraps is used.

Show the explanation

A decorator is a function that takes a function and returns a function.

Python
@timed
def build_report(): ...

is exactly the same as build_report = timed(build_report), run once when the def executes.

This works because functions are first-class objects in Python: they can be passed around and returned. Decorators also rely on closures: an inner function remembers the variables of the function that created it.

Python
import functools, time

def timed(func):
    @functools.wraps(func)                 # copy the name, docstring and so on onto wrapper
    def wrapper(*args, **kwargs):          # accept any signature
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:                           # report even if func raises
            print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
    return wrapper

Why `functools.wraps`? Without it, build_report.__name__ would be "wrapper" and its docstring would be lost. That confuses logs, debuggers, help() and frameworks that look up functions by name. wraps copies __name__, __doc__, __module__ and __qualname__, and sets __wrapped__ so the original function can still be reached.

Decorators with arguments need one more level, a function that returns a decorator:

Python
def retry(times):
    def deco(func):
        @functools.wraps(func)
        def wrapper(*a, **k):
            for attempt in range(times):
                try:
                    return func(*a, **k)
                except Exception:
                    if attempt == times - 1:
                        raise
        return wrapper
    return deco

@retry(3)
def fetch(): ...

Decorators you will meet: @staticmethod, @classmethod, @property, @functools.lru_cache (memoisation), @dataclasses.dataclass, and route decorators in Flask or FastAPI (@app.get("/")). Stacked decorators apply from the bottom up.

Original UniCircle question. Last reviewed 22 Oct 2026.