Python Decorators

Add behaviour to functions without changing their code, using decorators, wrapper functions, functools.wraps and decorators that take arguments.

Lesson 43 of 107Advanced17 min read

Imagine you want to time how long several functions take, or log every call, or check a user is logged in before a function runs. You could paste the same lines into the top and bottom of every function, but that's repetitive and clutters the code that actually matters.

A decorator solves this neatly. It's a function that takes another function and returns an improved version of it. You apply it by writing @decorator_name on the line above a function definition, and the extra behaviour is added without touching the function's own code.

You've probably already seen decorators such as @property or @app.route("/") in web frameworks. In this lesson you'll learn the two ideas decorators are built on, write your own step by step, make them work with any function, give them arguments, and use the helpful decorators that come with Python.

Functions are objects

In Python, a function is a value like any other. You can store it in a variable, pass it to another function and return it from a function:

Python
def shout(text):
    return text.upper() + "!"

say = shout             # no brackets: the function itself
print(say("hello"))     # HELLO!

A function can also be defined inside another function. The inner function can use the outer function's variables, even after the outer function has finished. This is called a closure, and it's what lets a decorator remember the function it wraps.

Writing a decorator

Python
def announce(func):
    def wrapper():
        print("Starting...")
        func()
        print("Done.")
    return wrapper

@announce
def backup():
    print("Backing up files")

@announce is shorthand for backup = announce(backup). The name backup now refers to wrapper, which prints a message, calls the original function, then prints another. Calling backup() prints all three lines.

Wrapping any function

Real decorators must work with functions that take arguments and return values. Use *args and **kwargs to pass everything through, and return the result:

Python
import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}{args}")
        result = func(*args, **kwargs)
        return result
    return wrapper

@functools.wraps(func) copies the original function's name and docstring onto the wrapper. Without it, every decorated function would be called "wrapper" in error messages and help text.

Writing a decorator that takes arguments

Here is the three-layer pattern in full. The outer function receives the settings, the middle one receives the function, and the inner wrapper does the work on every call:

Python
import functools

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

@repeat(times=3)
def cheer(name):
    print(f"Go {name}!")

cheer("Ada")

This prints Go Ada! three times. @repeat(times=3) first calls repeat(3), which returns decorator; Python then applies that to cheer. Stacked decorators are applied from the bottom up: the one closest to the def wraps the function first.

Decorators with arguments, and built-ins

To give a decorator settings, such as @retry(times=3), add one more layer: a function that takes the settings and returns the decorator. It's three nested functions, which looks complex at first but always follows the same pattern.

Python includes several useful decorators:

  • @functools.lru_cache remembers results, so repeat calls with the same arguments return instantly.
  • @property lets a method be read like an attribute, as in account.balance.
  • @staticmethod and @classmethod change how methods in a class receive their first argument.
  • @dataclass writes a class's boilerplate for you, as you'll see in the dataclasses lesson.

Python Decorators Syntax

Decorator template

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # before
        result = func(*args, **kwargs)
        # after
        return result
    return wrapper

@decorator
def my_function():
    ...

Python Decorators Summary

  • Functions are objects: they can be passed to and returned from other functions.
  • A decorator takes a function and returns a new function that adds behaviour.
  • @decorator above a definition means func = decorator(func).
  • Use *args, **kwargs and return the result so the wrapper works for any function; add @functools.wraps.
  • Built-in decorators include lru_cache, property, staticmethod, classmethod and dataclass.

Python Decorators Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A timing decorator

Python
import functools
import time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        seconds = time.perf_counter() - start
        print(f"{func.__name__} took {seconds:.2f}s")
        return result
    return wrapper

@timed
def slow_add(a, b):
    time.sleep(0.5)
    return a + b

print(slow_add(2, 3))
Output
slow_add took 0.50s
5
Line-by-line explanation
  1. Line 7Record the time before calling the original function.
  2. Line 8Pass every argument through unchanged.
  3. Line 11Return the original result, so slow_add still returns 5.
  4. Line 14The decorator is applied once, when the function is defined. The exact time may differ slightly.
Example 2

A decorator with an argument

Python
import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(times=3)
def cheer(name):
    print(f"Go {name}!")

cheer("team")
Output
Go team!
Go team!
Go team!
Line-by-line explanation
  1. Line 3The outer function receives the setting.
  2. Line 4It returns the real decorator, which receives the function.
  3. Line 12repeat(times=3) runs first and returns decorator, which then wraps cheer.
Example 3

Caching with lru_cache

Python
from functools import lru_cache

@lru_cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(80))
print(fib.cache_info().hits)
Output
23416728348467685
78
Line-by-line explanation
  1. Line 3The cache stores every result.
  2. Line 7Without the cache, fib(80) would make billions of calls. With it, each value is calculated once.
  3. Line 10hits counts how many calls were answered from the cache.

Common Python Decorators Mistakes

Calling the function in the decorator line

Don't
@timed()
def report():
    ...
Do
@timed
def report():
    ...

timed doesn't take settings, so it shouldn't have brackets. Brackets are only for decorators like repeat(times=3).

Forgetting to return the result

Don't
def wrapper(*args, **kwargs):
    func(*args, **kwargs)        # result is lost
Do
def wrapper(*args, **kwargs):
    return func(*args, **kwargs)

Losing the function's name

Don't
def log_calls(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
# report.__name__ is now 'wrapper'
Do
def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Python Decorators in Real Projects

Decorators in frameworks

Flask and FastAPI use decorators to connect web addresses to functions, as in @app.get("/users"). Testing tools use them to mark tests and supply data. Django uses @login_required to protect pages, and many libraries offer @retry decorators that try a network call again when it fails. Once you understand the pattern, all of these become much less mysterious.

Python Decorators FAQ

What is a decorator in Python, in simple terms?

A decorator is a function that wraps another function to add behaviour before or after it runs, without editing the original function's code. Writing @timer above def build(): is shorthand for build = timer(build).

What is a closure and why do decorators need one?

A closure is an inner function that remembers variables from the function that created it, even after that outer function has returned. The wrapper inside a decorator is a closure: it remembers func, the original function, so it can call it later.

Why should I use functools.wraps?

Without it, the decorated function takes on the wrapper's name and docstring, so help(), error messages, logging and testing tools all show wrapper. @functools.wraps(func) copies the original name, docstring and other metadata onto the wrapper.

Can I use more than one decorator on a function?

Yes. Stack them on separate lines. They are applied from the bottom up, so in @a then @b above a function, b wraps the function first and a wraps the result. Order matters when, for example, one decorator caches results and another checks permissions.

Can decorators be used on classes and methods?

Yes. Decorators work on methods in exactly the same way, as long as the wrapper accepts *args, which includes self. A decorator can also be applied to a whole class, which is how @dataclass adds methods to the class it decorates.

Python Decorators Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the line that applies the timed decorator to the function.

timed
def build_report():
    ...
Multiple choiceeasy

What is @shout above def hello(): shorthand for?

Predict the outputmedium

What does this print?

def twice(func):
    def wrapper():
        func()
        func()
    return wrapper

@twice
def hi():
    print("hi")

hi()
Multiple choicemedium

Why does a wrapper use *args, **kwargs?

Multiple choicehard

What does @functools.wraps(func) do?

Python Decorators Quiz

5 questions to check what you learned.

  1. What is a decorator?

    • A function that takes a function and returns a modified version of it
    • A comment that styles code
    • A type of class
    • A loop
  2. What is a closure?

    • A function with no return
    • An inner function that remembers variables from the function it was defined in
    • The end of a file
    • A closed file object
  3. What does @lru_cache do?

    • Stores results so repeated calls with the same arguments return instantly
    • Deletes old functions
    • Logs every call
    • Limits recursion
  4. How many nested functions does a decorator with arguments normally have?

    • One
    • Two
    • Three
    • Four
  5. When does a decorator run?

    • Every time the program starts a loop
    • Once, when the decorated function is defined; the wrapper runs on each call
    • Only when an error happens
    • Never, it's just a label