Skip to content
Theory · Functions as values · lesson 7 of 10

Decorators

Picture a present in wrapping paper. The present inside is the same, but to get to it, you unwrap the paper first. In Python, you can "wrap" a function the same way: every call first goes through the wrapping and only then reaches the function itself.

Why would you need it? Before each call of several functions, the line calling must be printed. You could add a print to the body of each of them, but then the same line repeats in many places. Instead, the function is passed to another function, which returns a new one with the added behavior:

Python
def announce(func):
    def wrapper(*args, **kwargs):
        print("calling")
        return func(*args, **kwargs)
    return wrapper


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


add = announce(add)
result = add(2, 3)  # calling
print(result)  # 5

Let's walk through the example line by line:

  1. add = announce(add) hands the function add to announce and gets wrapper back. Now the name add refers to it.
  2. add(2, 3) calls wrapper, which prints calling.
  3. wrapper calls the original add with the same arguments and returns its result, 5.
  4. print(result) prints 5.

The wrapper

A function that takes a function and returns a new one is called a decorator. The function it returns is a wrapper: that very wrapping paper.

Here wrapper is a closure: it remembers func, the original add. When called, the wrapper prints calling, then calls the original function and returns its result.

The *args and **kwargs parameters let the wrapper accept any arguments and pass them to the original function unchanged. That is why announce fits a function with any parameters. Before and after the call, the wrapper can do anything:

  • print a line;
  • count calls;
  • change the result.

The line add = announce(add) calls announce once. The body of add does not run at that point: announce receives the function, not the result of calling it. After the assignment, every call of add goes through the wrapper. Only the wrapper itself can reach the original function, through func.

This is where the present picture stops working. Real paper is taken off once, but a wrapper runs on every call. It can also swap the result on the way back—which never happens to a present.

The @ syntax

The line @announce above a def does the same thing more briefly. It equals the assignment greet = announce(greet) right after the def:

Python
@announce
def greet(name):
    return f"hi {name}"


message = greet("ann")  # calling
print(message)  # hi ann

One decorator fits many functions: the line @announce goes above each of them, while print is written once, inside wrapper.

The decorator runs once, when Python executes the def, not on every call. announce itself prints nothing at that point. calling appears on each call of greet, because the wrapper prints it.

Interviewers like to ask this: "What is a decorator, and what does @dec stand for?" The answer has two parts:

  • a decorator is a function that takes a function and returns another one, usually a wrapper;
  • @dec above def f is short for f = dec(f), and from then on the name f refers to whatever dec returned.

Common mistake

The wrapper is missing return:

Python
def announce(func):
    def wrapper(*args, **kwargs):
        print("calling")
        func(*args, **kwargs)  # ✗
    return wrapper


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


print(add(2, 3))

The program prints calling and then None instead of 5. The original add returned 5 to the wrapper, but the wrapper did not return it, and a function without return returns None. Write return func(*args, **kwargs).

Key points

  • A decorator takes a function and returns a wrapper that adds behavior before or after the call.
  • @dec above def f is the same as f = dec(f) right after the definition.
  • A wrapper with *args, **kwargs fits a function with any parameters.
  • The wrapper must return the original function's result, or the call returns None.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way