Python Tutorial › Functions › Decorators

Python Decorators

AdvancedLesson 30 of 4712 min read7 runnable examples

A decorator adds behaviour to a function without changing its code: logging, timing, retrying, checking permissions. You meet decorators everywhere in test frameworks, such as @pytest.fixture and @pytest.mark.parametrize. This lesson builds one from scratch.

Advertisement
In this lesson
  • Why decorators work: functions are objects
  • Write a wrapper and apply it with @
  • Keep the function’s name with functools.wraps
  • Write decorators that take arguments

Functions are objects

A decorator relies on two facts: a function can be passed to another function, and a function can create and return a new function.

Python
def shout(text):    return text.upper() def call_twice(func, value):    return func(value), func(value) print(call_twice(shout, "hi"))
Output
('HI', 'HI')

A decorator by hand

A decorator is a function that takes a function and returns a new function, usually a wrapper that runs extra code around the original:

Python
def log_calls(func):    def wrapper(*args, **kwargs):        print(f"calling {func.__name__} with {args}")        result = func(*args, **kwargs)        print(f"{func.__name__} returned {result}")        return result    return wrapper def add(a, b):    return a + b add = log_calls(add)      # replace add with the wrapped versionadd(2, 3)
Output
calling add with (2, 3)
add returned 5

The @ syntax

@log_calls above a definition is a shortcut for add = log_calls(add):

Python
def log_calls(func):    def wrapper(*args, **kwargs):        print(f"calling {func.__name__}{args}")        return func(*args, **kwargs)    return wrapper @log_callsdef multiply(a, b):    return a * b print(multiply(4, 5))
Output
calling multiply(4, 5)
20
Note

The wrapper uses *args, **kwargs so it can pass along whatever arguments the decorated function takes.

Keep the original name with functools.wraps

Without help, the decorated function reports the wrapper’s name and loses its docstring. functools.wraps copies them over:

Python
from functools import wraps def plain(func):    def wrapper(*args, **kwargs):        return func(*args, **kwargs)    return wrapper def proper(func):    @wraps(func)    def wrapper(*args, **kwargs):        return func(*args, **kwargs)    return wrapper @plaindef first():    """First docstring.""" @properdef second():    """Second docstring.""" print(first.__name__, first.__doc__)print(second.__name__, second.__doc__)
Output
wrapper None
second Second docstring.

Always use @wraps in your decorators.

A timing decorator

Python
import timefrom functools import wraps def timer(func):    @wraps(func)    def wrapper(*args, **kwargs):        start = time.perf_counter()        result = func(*args, **kwargs)        elapsed = time.perf_counter() - start        print(f"{func.__name__} took {'under' if elapsed < 0.5 else 'over'} 0.5 s")        return result    return wrapper @timerdef slow_sum(n):    time.sleep(0.1)    return sum(range(n)) print(slow_sum(1000))
Output
slow_sum took under 0.5 s
499500

Decorators with arguments

To pass settings such as @retry(times=3), add one more outer function. retry(times=3) runs first and returns the actual decorator.

Python
from functools import wraps def retry(times):    def decorator(func):        @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}")            raise RuntimeError(f"{func.__name__} failed {times} times")        return wrapper    return decorator calls = {"n": 0} @retry(times=3)def flaky():    calls["n"] += 1    if calls["n"] < 3:        raise ValueError("server busy")    return "success" print(flaky())
Output
attempt 1 failed: server busy
attempt 2 failed: server busy
success

Stacking decorators

Several decorators apply from the bottom up: the one closest to the function wraps it first.

Python
def bold(func):    return lambda: "<b>" + func() + "</b>" def italic(func):    return lambda: "<i>" + func() + "</i>" @bold@italicdef label():    return "Python" print(label())
Output
<b><i>Python</i></b>

Built-in decorators you will meet

DecoratorWhat it does
@staticmethod, @classmethodMethods that do not need an instance (see Classes & Objects)
@propertyCall a method like an attribute
@functools.lru_cacheRemember results of expensive calls
@dataclasses.dataclassGenerate __init__, __repr__ and more for a class
@pytest.fixture, @pytest.mark.parametrizeSet up test data and run a test with many inputs

Check your understanding

Predict the output

def twice(func):    def wrapper(x):        return func(func(x))    return wrapper @twicedef add3(x):    return x + 3 print(add3(10))

Quick quiz

1. What is @deco above def f(): equivalent to?

2. Why use functools.wraps?

3. With @a above @b above def f, which wraps f first?

Exercise: count calls

Write a decorator count_calls that counts how many times a function has been called and stores the number in wrapper.calls.

Input
call greet("Asha"), greet("Ravi"), then print greet.calls
Expected output
Hello Asha
Hello Ravi
calls: 2
Hint 1

Set wrapper.calls = 0 before returning the wrapper, and add 1 inside it.

Program Practice

Python decorators FAQ

What is a decorator in Python?

A decorator is a function that takes another function and returns a modified version of it, usually a wrapper that adds behaviour before or after the original runs.

What does functools.wraps do?

It copies the wrapped function’s name, docstring and other metadata onto the wrapper, so debugging and documentation tools see the original function.

Can a decorator take arguments?

Yes. Write a function that takes the arguments and returns the decorator, giving three levels: def retry(times): def decorator(func): def wrapper(...).

2 thoughts on “Python Decorators”

Leave a Comment