Know some Python? Write your own interactive tutorial, with runnable examples and exercises, and share it with a link. Write and share your own tutorial.

Start writing

Learn Python / Decorators

Decorators

You've already seen a few lines that start with @, like @lru_cache and @dataclass. These are decorators: a way to add extra behaviour to a function without changing the function's own code. Timing, logging, caching and checking permissions are all commonly done with decorators.

Decorators build on two ideas from Part 3: functions are values you can pass around, and inner functions can remember variables from the function around them.

Step 1: functions can be passed around

A function name without () is the function itself, and you can pass it to another function:

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

def call_twice(func, value):
    print(func(value))
    print(func(value))

call_twice(shout, "hello")
Output

Step 2: functions can create functions

A function can define a new function inside itself and return it:

def make_greeter(greeting):
    def greet(name):
        return f"{greeting}, {name}!"
    return greet

say_hi = make_greeter("Hi")
say_hello = make_greeter("Hello")

print(say_hi("Ada"))
print(say_hello("Grace"))
Output

Each call to make_greeter creates a new greet function, and each one remembers the greeting it was made with (this is the closure from the Scope lesson). say_hi always says "Hi" and say_hello always says "Hello", even though make_greeter finished running long ago.

Step 3: wrapping a function

Put those together: a function that takes a function, builds a new function around it, and returns the new one. The new function, usually called wrapper, does something extra and calls the original:

def announce(func):
    def wrapper():
        print("About to run...")
        func()
        print("Done!")
    return wrapper

def say_hello():
    print("Hello!")

say_hello = announce(say_hello)
say_hello()
Output

announce is a decorator. The line say_hello = announce(say_hello) replaces say_hello with the wrapped version. Step by step:

  1. announce(say_hello) runs once. It defines wrapper (without running it) and returns it. Inside wrapper, func still refers to the original say_hello.
  2. say_hello = ... points the name say_hello at wrapper.
  3. Now every say_hello() call actually runs wrapper(): it prints About to run..., calls the original function (Hello!), then prints Done!.

The original function's code was never edited; it just got wrapped.

The @ syntax

Writing say_hello = announce(say_hello) is clumsy, so Python has a shortcut. Put @announce on the line above the function:

def announce(func):
    def wrapper():
        print("About to run...")
        func()
        print("Done!")
    return wrapper

@announce
def say_hello():
    print("Hello!")

say_hello()
Output

@announce above def say_hello means exactly the same as say_hello = announce(say_hello) after it. The decorator runs once, when Python reaches the def, not each time the function is called. Each call then runs the wrapper. Write the decorator name without (): @announce, not @announce().

Decorating functions with arguments

Our wrapper takes no arguments, so it only works on functions without any. To wrap any function, let the wrapper accept any arguments with *args and **kwargs (from the Function Arguments lesson) and pass them on. Also return the original function's result:

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

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

@log_calls
def greet(name, punctuation="!"):
    return "Hi " + name + punctuation

add(2, 3)
greet("Ada", punctuation="?")
Output

For add(2, 3), the wrapper receives args = (2, 3) and kwargs = {}, and passes them straight on with func(*args, **kwargs). For greet("Ada", punctuation="?"), args is ("Ada",) and kwargs is {'punctuation': '?'}. func.__name__ is the original function's name, which makes the log readable.

Don't forget return result. Without it, the wrapper returns None, so add(2, 3) would give None even though the original add returned 5. The wrapper takes the original's place, so it must hand back whatever the original returned.

This shape works as a template for almost every decorator you'll write:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        # do something before
        result = func(*args, **kwargs)
        # do something after
        return result
    return wrapper

A practical decorator: timing

Here's a decorator that measures how long a function takes:

import time

def timed(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper

@timed
def total_of_squares(n):
    return sum(i * i for i in range(n))

print(total_of_squares(1_000_000))
Output

Once written, you can add @timed to any function you want to measure, without touching its code.

Keeping the function's name with functools.wraps

A plain wrapper hides the original function's name and docstring:

def announce(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@announce
def say_hello():
    """Print a greeting."""
    print("Hello!")

print(say_hello.__name__)
print(say_hello.__doc__)
Output

It prints wrapper and None: the name say_hello now refers to the wrapper, so you see the wrapper's name and its (missing) docstring. That makes error messages and help() confusing.

Add @functools.wraps(func) to the wrapper to copy them across. It's good practice in every decorator:

import functools

def announce(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@announce
def say_hello():
    """Print a greeting."""
    print("Hello!")

print(say_hello.__name__)
print(say_hello.__doc__)
Output

Decorators you'll meet

Python and its libraries come with many ready-made decorators:

Decorator What it does
@functools.lru_cache remembers results so repeated calls are instant
@dataclasses.dataclass writes __init__, __repr__ and __eq__ for a class
@staticmethod / @classmethod methods that don't need an object
@property lets a method be read like an attribute

@property is a nice one to know:

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return round(3.14159 * self.radius ** 2, 2)

c = Circle(3)
print(c.area)      # no () needed
Output

area is written as a method, but @property lets you read it like an attribute, without (). It's worked out fresh each time you read it, so after c.radius = 10, c.area gives the new area. Assigning c.area = 5 is an AttributeError, because there's no stored value to change. That makes @property handy for values that are always calculated from other attributes.

Going deeper: Decorators that take arguments optional

Sometimes you want to configure a decorator, as in @repeat(3). That needs one more layer: a function that takes the settings and returns a decorator:

import functools

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

@repeat(3)
def cheer():
    print("Hip hip hooray!")

cheer()
Output

@repeat(3) first calls repeat(3), which returns decorator. Then decorator wraps cheer.

Going deeper: Stacking decorators optional

You can put several decorators on one function. They're applied from the bottom up, so the one closest to the function wraps it first:

def bold(func):
    def wrapper():
        return "<b>" + func() + "</b>"
    return wrapper

def italic(func):
    def wrapper():
        return "<i>" + func() + "</i>"
    return wrapper

@bold
@italic
def hello():
    return "hello"

print(hello())
Output

Exercises

Exercise 1: Before and after

Write a decorator sandwich that prints --- before and after calling the function. Calling say_hi() should print three lines: ---, Hi! and ---.

def sandwich(func):
    pass

@sandwich
def say_hi():
    print("Hi!")

say_hi()
Output

Exercise 2: Shout the result

Write a decorator uppercase that calls the function and returns its result in capitals. It should work with any arguments.

def uppercase(func):
    pass

@uppercase
def greet(name):
    return f"hello, {name}"

print(greet("ada"))   # HELLO, ADA
Output

Exercise 3: Count the calls

Write a decorator count_calls that counts how many times the function has been called and prints Call number N each time. You can store the count as an attribute on the wrapper, like wrapper.calls = 0.

def count_calls(func):
    pass

@count_calls
def ping():
    print("ping")

ping()
ping()
ping()
Output

Exercise 4: Only positive numbers

Write a decorator positive_only that raises a ValueError if any argument is a negative number, and otherwise calls the function normally.

def positive_only(func):
    pass

@positive_only
def area(width, height):
    return width * height

print(area(3, 4))

try:
    area(3, -4)
except ValueError as e:
    print("Error:", e)
Output

Summary

  • A decorator is a function that takes a function and returns a new, wrapped function.
  • @decorator above a def is a shortcut for func = decorator(func).
  • Use *args and **kwargs in the wrapper so it works with any arguments, and return the result.
  • Add @functools.wraps(func) to keep the original name and docstring.
  • Decorators add behaviour like timing, logging or caching without changing the function itself.
  • Built-in decorators include @property, @staticmethod, @classmethod and @functools.lru_cache.

Share this tutorial

Know something worth teaching? Write your own tutorial with runnable examples and share it.