Chapters
Python114 chapters

FunctionsChapter 56 of 114

Decorators

Wrap a function to add behaviour, without editing the function.

A function that wraps a function

A decorator takes a function and returns a replacement. Since functions are ordinary values, nothing new is needed to write one:

Python
def loud(fn):
    def wrapper(*args, **kwargs):
        print("calling", fn.__name__)
        result = fn(*args, **kwargs)
        print("done")
        return result
    return wrapper

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

add = loud(add)
print(add(2, 3))

Output

calling add
done
5

The @ is shorthand

@loud above a def means exactly add = loud(add):

Python
def loud(fn):
    def wrapper(*args, **kwargs):
        print("calling", fn.__name__)
        return fn(*args, **kwargs)
    return wrapper

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

print(add(2, 3))

Output

calling add
5

The *args, **kwargs matter: the wrapper has to accept whatever the original did, and pass it straight through.

It hides the original

The name and docstring now belong to the wrapper, which breaks help() and confuses tracebacks:

Python
def loud(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@loud
def add(a, b):
    """Add two numbers."""
    return a + b

print(add.__name__)
print(add.__doc__)

Output

wrapper
None

functools.wraps copies the metadata across. Always use it:

Python
from functools import wraps

def loud(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@loud
def add(a, b):
    """Add two numbers."""
    return a + b

print(add.__name__)
print(add.__doc__)

Output

add
Add two numbers.

A decorator that takes arguments

This needs one more layer: a function that returns a decorator.

Python
from functools import wraps

def repeat(times):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = fn(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet():
    print("hello")

greet()

Output

hello
hello
hello

Read @repeat(3) as: call repeat(3), which gives back a decorator, then apply that. Three layers looks like a lot, and it is the standard shape.

Stacking

They apply bottom up — the one nearest the def wraps first:

Python
from functools import wraps

def tag(name):
    def decorator(fn):
        @wraps(fn)
        def wrapper():
            return f"<{name}>{fn()}</{name}>"
        return wrapper
    return decorator

@tag("b")
@tag("i")
def text():
    return "hi"

print(text())

Output

<b><i>hi</i></b>

Ones you already use

Python
from functools import lru_cache

@lru_cache
def slow_square(n):
    return n * n

print(slow_square(4))
print(slow_square(4))
print(slow_square.cache_info().hits)

Output

16
16
1

@property, @staticmethod, @classmethod and @dataclass are all decorators too. You have been using them since the classes chapters.

Keep them boring

A decorator runs on every call, and it is invisible at the call site. That makes it perfect for cross-cutting concerns — timing, caching, logging, retries, access checks — and a poor place for anything a reader needs to know about to understand what the function returns.

Test yourself

2 questions

What does @loud above a def actually do?

Show the answer

Replaces the function with loud(function) — The @ is only shorthand for a reassignment. Nothing magic is happening.

Why use functools.wraps inside a decorator?

Show the answer

It copies the original's name and docstring onto the wrapper — Without it, help(), tracebacks and introspection all show the wrapper instead.

Next chapter

Generators

Produce values one at a time instead of building a whole list.