This article is an overview of Python decorators trying to cover many aspects starting from the basic steps and usages and moving into complex class-based decorators for asyncronious programming. Decorator is a simple yet powerful syntatic sugar in Python language that can make a life of a developer easier and keep codebase clean.

The official Python 3 documentation provides a very short description of decorators:

A function returning another function, usually applied as a function transformation using the @wrapper syntax. Common examples for decorators are classmethod() and staticmethod().

The decorator syntax is merely syntactic sugar, the following two function definitions are semantically equivalent:

def f(...):
    ...
f = staticmethod(f)

@staticmethod
def f(...):
    ...

The same concept exists for classes, but is less commonly used there. See the documentation for function definitions and class definitions for more about decorators.

Though this description is valid it provides very little information about the true abilities of the subject. And that’s what we’ll try to fix.

Index

Creating the simplest decorator

Decorators are wrappers around functions and classes providing additional functionality around the wrapped objects. To understand how decorators work first we need to look into closures. Closure is a way to create functions with a specific scope. For example:

def wait_generator(sec):
    def inner():
        print(f'waiting for {sec} seconds...')
        time.sleep(sec)

    return inner

Note, that inner() function uses sec argument, which is not passed directly into it, but available for it in scope of wait_generator() function. This provides a template that allows to create different wait functions:

wait_2_sec = wait_generator(2)
wait_3_sec = wait_generator(3)

wait_2_sec()
>>> waiting for 2 seconds...
wait_3_sec()
>>> waiting for 3 seconds...

The same way we pass sec as argument to a wait_generator we can pass another function. Let’s create a function that will take another method as agrument and print some nice message before and after its execution.

def execute_function(func):
    def inner():
        print(f'About to run "{func.__name__}()"')
        func()
        print(f'"{func.__name__}()" run complete')

    return inner

And we’ll take a simple func that prints out “Hello”:

def hello():
    print('Hello')

Now we can combine them together:

wrapped_hello = execute_function(hello)

wrapped_hello()
>>> About to run "hello()"
>>> Hello
>>> "hello()" run complete

To make this closure be a real decorator we need to reassign the result of the execute_function to the same func we passed as argument:

hello()  # before wrapper function
>>> Hello

hello = execute_function(hello)

hello()  # after wrapper function
>>> About to run "hello()"
>>> Hello
>>> "hello()" run complete

That’s it! We created our first decorator: execute_function, because this:

def hello():
    print('Hello')

hello = execute_function(hello)

can be rewritten as:

@execute_function
def hello():
    print('Hello')

Getting more real

Handle function arguments and return value

We can make our example more real-life to support func to have arguments and also make the wrapper return the result of the func. We’ll create a function that returns the mathematical sum of all its arguments. Note, that it’s specifically made non-optimised and slow for demonstration purpose only (also because I didn’t think of an interesting one):

def sum_numbers(*args):
    return sum(args)

But our execute_function does not know in advance what func will be passed and how many arguments it can have, so we make it as generic as possible:

def execute_function(func):
    def inner(*args, **kwargs):
        print(f'About to run "{func.__name__}()"')
        result = func(*args, **kwargs)
        print(f'"{func.__name__}()" run complete')
        return result

    return inner

Note, that execute_function signature doesn’t change: it still takes only func as argument, but inner now supports to have arguments that will be passed to func:

sum_numbers = execute_function(sum_numbers)

result = sum_numbers(5, 4)
>>> About to run "sum_numbers()"
>>> "sum_numbers()" run complete
print(result)
>>> 9

Decorator to measure function execution time

Now that we know how decorators work let’s modify execute_function to measure inner function execution time instead of just priting lines of text.

import time

def measure_time(func):
    def inner(*args, **kwargs):
        start_t = time.time()
        result = func(*args, **kwargs)
        end_t = time.time()
        print(f'Execution time: {end_t - start_t}s')
        return result

    return inner

And we can apply it to a function to record its run time:

@measure_time
def sum_numbers(*args):
    return sum(args)

sum_numbers(4, 5, 6)  # 15
>>> Execution time: 2.86102294921875e-06s
sum_numbers(*range(int(1E+06)))  # 499999500000
>>> Execution time: 0.022812366485595703s

Parametrized decorator

For now our decotrator always prefixes the output string with “Execution time” phrase, but what if we want to make it configurable? E.g. consider this example:

@measure_time(text='Time spent')
def sum_numbers(*args):
    return sum(args)

sum_numbers(4, 5, 6)
>>> Time spent: 2.87491942710492e-06s

In order to achieve this we need to remember that decorator in nothing more than a function itself and it can handle parameters as any other function. First let’s remind us how decorator without any arguments looks like. We’ll write it first using decorator syntax and then its corresponding function syntax below:

@measure_time
def func(*args, **kwargs):
   ...

func = measure_time(func)

Then decorator with arguments should look like:

@measure_time(text='Time spent')
def func(*args, **kwargs):
   ...

func = measure_time(text='Time spent')(func)

This is a very important moment here: now measure_time should accept argument(s) and instead of being a decorator should return a decorator method. So a code for measure_time will look like this:

import time

def measure_time(text=None):
    text = text if text is not None else 'Execution time'
    def internal(func):
        def wrapper(*args, **kwargs):
            start_t = time.time()
            result = func(*args, **kwargs)
            end_t = time.time()
            print(f'{text}: {end_t - start_t}s')
            return result
        return wrapper
    return internal

@measure_time(text='Time spent')
def sum_numbers(*args):
    return sum(args)

sum_numbers(4, 5, 6)
>>> Time spent: 1.9073486328125e-06s

# As we made `text` optional it's possible to run decorator without arguments.
# Note that parentheses are still required.
@measure_time()
def sum_numbers(*args):
    return sum(args)

sum_numbers(4, 5, 6)
>>> Execution time: 2.86102294921875e-06s

Advanced usage

Decorator with optional arguments

So we created a decorator that accepts an argument, but now we have a problem: it’s not possible to use decorator without parentheses because only the call to our measure_time function returns a decorator. Would be nice to be able to support both behaviours so we don’t need extra pair of parentheses when we don’t use any arguments:

@measure_time
def sum_numbers(sec):
    ...

@measure_time(text='Time spent')
def multiply_numbers(sec):
    ...

To distinguish between these two calls we can check what the first decorator argument is as in the first case it’ll be a function:

func = measure_time(func)

and in the second example - a text parameter:

func = measure_time(text='Time spent')(func)

Then the code for measure_time will be:

import time

def measure_time(*decorator_args, **decorator_kwargs):
    def internal(func):
        def wrapper(*args, **kwargs):
            start_t = time.time()
            result = func(*args, **kwargs)
            end_t = time.time()
            print(f'{text}: {end_t - start_t}s')
            return result
        return wrapper

    text = 'Execution time'
    if len(decorator_args) == 1 and callable(decorator_args[0]):
        return internal(decorator_args[0])

    text = decorator_kwargs.get('text', text)
    return internal

And now it’s possible to use both syntaxes:

@measure_time(text='Time spent')
def sum_numbers(*args):
    return sum(args)

sum_numbers(4, 5, 6)
>>> Time spent: 1.9073486328125e-06s

@measure_time
def sum_numbers(*args):
    return sum(args)

sum_numbers(4, 5, 6)
>>> Execution time: 2.86102294921875e-06s

Class-based decorator

Decorator is a syntactic sugar not only for functions but also for classes. In our last examples we had a measure_time function that returns internal that returns wrapper - not a very straight-forward code. Plus note that we are dealing with a very simple example to only measure function execution time. Real-life decorators are much complex and can make code hard to comprehend.

Class-based decorator can help make our code a bit cleaner and easier to read in this case.

To understand how to create such class let’s look again at decorator shorcut syntax:

sum_numbers = measure_time(sum_numbers)
sum_numbers(5, 6, 7)

Imagine measure_time to be not a funciton, but a class. Then on the first line we create a class object, passing sum_numbers into class __init__ method. And on the second line we call this object with arguments. So it means that a class that can be used as decorator should define two methods: __init__ and __call__:

class measure_time:
    # The decorated function is passed to class __init__ method
    def __init__(self, func):
        ...

    # __call__ is called when decorated function is executed
    def __call__(self, *args, **kwargs):
        ...

Let’s rewrtie out function-based decorator without any arguments into a class-based:

import time

def measure_time(func):
    def inner(*args, **kwargs):
        start_t = time.time()
        result = func(*args, **kwargs)
        end_t = time.time()
        print(f'Execution time: {end_t - start_t}s')
        return result
    return inner

class measure_time:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        start_t = time.time()
        result = self.func(*args, **kwargs)
        end_t = time.time()
        print(f'Execution time: {end_t - start_t}s')
        return result

Those two implemetations are identical and provide the same functionality:

@measure_time
def sum_numbers(*args):
    return sum(args)

sum_numbers(4, 5, 6, 7)
>>> Execution time: 2.86102294921875e-06s

Class-based decorator with arguments

Let’s rewrite our decorator with optional arguments into a class-based. First we’ll remind ourselves how a decorator with arguments looks like not in a short-cut form:

sum_numbers = measure_time(text='Time spent')(sum_numbers)
sum_numbers(4, 5, 6, 7)

When we were dealing with functions measure_time was a function that produced a function that itself returned a function (not very pretty). Now that we switched to class-based logic, measure_time(text='Time spent') should be a class __init__, and (sum_numbers) call will be handled in __call__ logic being a usual simple function-based decorator without any arguments.

import time

class measure_time:
    def __init__(self, text=None):
        self.text = text or 'Execution time'

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            start_t = time.time()
            result = func(*args, **kwargs)
            end_t = time.time()
            print(f'{self.text}: {end_t - start_t}s')
            return result
        return wrapper

Now this class-based decorator can be used in a same way as a function-based:

@measure_time(text='Time spent')
def sum_numbers(*args):
    return sum(args)

sum_numbers(1, 2, 3)
>>> Time spent: 1.9073486328125e-06s

or using the default output:

# Note that parentheses are required.
@measure_time()
def sum_numbers(*args):
    return sum(args)

sum_numbers(1, 2, 3)
>>> Execution time: 2.1457672119140625e-06s

Class-based decorator with optional arguments

Now we ended up in a same not-very-pretty situation when using a decorator without any arguments we still need to use parentheses.

@measure_time()
def sum_numbers(*args):
    return sum(args)

Would be nice to rewrite our class-based decorator same way we did a function-based to support both usages:

@measure_time
def sum_numbers(*args):
    return sum(args)


@measure_time(text='Time spent')
def sum_numbers(*args):
    return sum(args)

To get the idea what we need to change let’s rewrite those decorator without any syntatic sugar:

# Let's call this CaseA
sum_numbers = measure_time(sum_numbers)
sum_numbers(4, 5, 6, 7)

and

# And this - CaseB
sum_numbers = measure_time(text='Time spent')(sum_numbers)
sum_numbers(4, 5, 6, 7)

It means that decorator __init__ should be able to handle both measure_time(sum_numbers) and measure_time(text='Time spent'). Then in CaseB the handling of measure_time(text='Time spent')(sum_numbers) should redirect the call back to CaseA __init__. This may sound a bit confusing, but the code below will show how it behaves:

import time

class measure_time:
    def __init__(self, func=None, text=None):
        # In CaseA func in not None and in CaseB - None
        self.func = func
        # In CaseA text is not present (the default one will be used)
        self.text = text or 'Execution time'

    def __call__(self, *args, **kwargs):
        if not self.func:
            # That will be CaseB:
            # the first and the only argument is a function to use the decorator
            # for; and we already saved a text to use in __init__.
            # So we emulate CaseA here but also passing text:
            return self.__class__(args[0], text=self.text)

        start_t = time.time()
        result = self.func(*args, **kwargs)
        end_t = time.time()
        print(f'{self.text}: {end_t - start_t}s')
        return result

This way it can handle both usages:

measure_time(text='Time spent')(sum_numbers)(1, 2, 3)
>>> Time spent: 1.9073486328125e-06s

# Note that empty parentheses are not required anymore
measure_time(sum_numbers)(1, 2, 3)
>>> Execution time: 1.6689300537109375e-06s

Decorator for a class

As you already guessed a decorator can be applied not only to functions but to classes. Same logic behind it applies. E.g. let’s create a decorator for a class that applies our measure_time functionality to all class methods:

@measure_time
class A:
    def x(self):
        return 10
    def y(self):
        return 20

a = A()
a.x()
>>> Execution time: 1.6689300537109375e-06s
a.y()
>>> Execution time: 1.6689300537109375e-06s

As before the decorator is a syntatic sugar:

class A:
    ...

A = measure_time(A)

That means that measure_time accepts class as an argument and should also return class modifying all its method in the process.

import time

def measure_time(cls):
    def method_decorator(func):
        def inner(*args, **kwargs):
            start_t = time.time()
            result = func(*args, **kwargs)
            end_t = time.time()
            print(f'Execution time: {end_t - start_t}s')
            return result
        return inner

    # SEarch all non-private class methods and apply method_decorator to them
    for func in filter(
        lambda func: not func.startswith('__') and callable(getattr(cls, func)),
        dir(cls)
    ):
        setattr(cls, func, method_decorator(getattr(cls, func)))
    return cls

Let’s put it to a test:

@measure_time
class A:
    x = 10
    def y(self):
        print(20)

a = A()
print(a.x)
>>> 10
a.y()
>>> 20
>>> Execution time: 9.5367431640625e-07s

Asynchronous decorators

Now we dive into Python asyncio library. We want to be able to apply our decorator to async funcitons like this:

@measure_time
async def sum_numbers(*args):
    return sum(args)

await sum_numbers(3, 4, 5)
>>> Execution time: 1.9489349310389372e-06s

We start again with function-based decorator without arguments. We need to be aware what to await and which methods will become async in decorator code now. Let’s write our async decorator without syntatic sugar:

await measure_time(sum_numbers)(3, 4, 5)

Now we can see that measure_time(sum_numbers) should not be async, but should return awaitable function.

import time

def measure_time(func):
    async def wrapper(*args, **kwargs):
        start_t = time.time()
        result = await func(*args, **kwargs)
        end_t = time.time()
        print(f'Execution time: {end_t - start_t}s')
        return result
    return wrapper

@measure_time
async def sum_numbers(*args):
    return sum(args)

await sum_numbers(3, 4, 5)
>>> Execution time: 1.9073486328125e-06s

Class-based async decorator with optional arguments

As you saw in a previous example async decorators don’t differ much from their sync versions. So we’ll discuss only the most complex case here. This example is similar to a corresponding synchronous one. We want to have a class-based async decorator that supports both usages:

@measure_time
async def sum_numbers(*args):
    return sum(args)

@measure_time(text='Time spent')
async def sum_numbers(*args):
    return sum(args)

Here again the first decorator call (__init__) should be sync and only __call__ - return an awaitable:

import time

class measure_time:
    def __init__(self, func=None, text=None):
        self.func = func
        self.text = text or 'Execution time'

    def __call__(self, *args, **kwargs):
        if not self.func:
            return self.__class__(args[0], text=self.text)

        async def wrapper(*args, **kwargs):
            start_t = time.time()
            result = await self.func(*args, **kwargs)
            end_t = time.time()
            print(f'{self.text}: {end_t - start_t}s')
            return result
        return wrapper(*args, **kwargs)

Now it can handle

@measure_time
async def sum_numbers(*args):
    return sum(args)

await sum_numbers(1, 2, 3)
>>> Execution time: 2.1457672119140625e-06s

and

@measure_time(text='Time spent')
async def sum_numbers(*args):
    return sum(args)

await sum_numbers(4, 5, 6)
>>> Time spent: 3.0994415283203125e-06s

Conclusion

Hope this article could cover some basics and give you the idea behind decorators. Of course you can go as crazy as you want when understanding this concept. E.g. mess around with data returned from a funciton:

# A decorator to convert meters to kilometers
def m_to_km(func):
    return lambda: func() / 1000.

@m_to_km
def get_distance():
    # some complex calculation here...
    return 10000

get_distance()
>>> 10.0

You are limited only by your imagination (and Python compiler) here.