Click to share! ⬇️

What Is A Python Decorator

A decorator in Python is a callable that takes another function as an argument and adds additional behavior to that function without explicitly modifying the function. A decorator has the ability to run additional code before and after each call to a function that it wraps. This means that decorators can access and modify input arguments and return values. The advantage of a decorator is that it will change the behavior of that function without permanently modifying it. In this tutorial, we’ll see how to create our own decorators as well as examine where decorators are used in popular python packages.


Functions In Python Are Objects

The first step in understanding Decorators in Python is understanding that a function is an object. Just like a string, float, int, and so on is an object in Python, so too is the function. Let’s use the Python type() function to demonstrate this.

some_str = 'some string'
a_bool = True
a_float = 1.0
a_int = 1
a_list = [1, 2, 3]
a_dict = {'a': 1, 'b': 2}
a_tuple = (1, 2, 3)
a_set = {1, 2, 3}

print(f'some_str: {type(some_str)}')
print(f'a_bool: {type(a_bool)}')
print(f'a_float: {type(a_float)}')
print(f'a_int: {type(a_int)}')
print(f'a_list: {type(a_list)}')
print(f'a_dict: {type(a_dict)}')
print(f'a_tuple: {type(a_tuple)}')
print(f'a_set: {type(a_set)}')
some_str: <class 'str'>
a_bool: <class 'bool'>
a_float: <class 'float'>
a_int: <class 'int'>
a_list: <class 'list'>
a_dict: <class 'dict'>
a_tuple: <class 'tuple'>
a_set: <class 'set'>

You can see that every one of these has a class keyword in its type output. In other words, they are all objects. Now check this out.

def my_func():
    print('my_func')


print(f'my_func: {type(my_func)}')
my_func: <class 'function'>

Inspecting the function with type() reveals that it has the same class signature as all the prior examples. In other words, a function is also an object! So what do we do with objects in Python? Well one thing we do with them is to pass them around between various functions and methods or assign them to other objects. It is this flexibility that makes decorators possible in Python.


Functions Inside Functions

It is perfectly legal to define a function inside of another function in Python. In this example here we simply define an inner function and then return it when the outer function is called.

def my_func():
    def inner_func():
        pass

    return inner_func


result = my_func()
print(result)
<function my_func.<locals>.inner_func at 0x000002D87FF46310>

You may also simply call a function inside of another function that has been defined elsewhere.

def random_func():
    print('Random stuff')


def my_func():
    random_func()


my_func()
Random stuff

You can also assign a function to some other variable and then use that new variable like a function itself by calling it with the () characters.

def my_func():
    print('Python is eating the world')


some_other_variable = my_func

some_other_variable()
Python is eating the world

How Do You Make A Decorator In Python?

We now see how functions in Python are flexible in that they can be passed to and returned from other functions, defined inside other functions, called inside other functions, and assigned to variables, among other possibilities. That leads us to look at the general syntax of a decorator function in Python.

Python Decorator Template

If you want to use a decorator on a function, you first have to write a decorator function. Most decorator functions follow a similar outline. You start by defining a function that accepts a function as a parameter. The name of this function will be the name of your decorator. This is seen in line 1 below. Inside of the decorator, a wrapper() function should be defined. We see this on line 4 below. In this example here, we do not do anything that would modify the original function. That is on purpose. We just want to see the skeleton outline of a typical decorator function here. Finally, the wrapper() function should return something, and lastly, we return the wrapper function itself.

def my_decorator(func):
    '''Decorator Function'''

    def wrapper():
        '''Wrapper Function'''
        result = func()
        return result

    return wrapper

A Function To Decorate

In order to use a decorator function, you need a function to decorate. Well here we go, let’s take a look at this function to decorate right here. It has the name to_be_decorated(). The only thing this function does is to return the string ‘output to decorate’. Below this, we print out the function itself, call the function and put the return value into the result variable. Lastly, we print out the result.

def to_be_decorated():
    return 'output to decorate'


print(to_be_decorated)
result = to_be_decorated()
print(result) 
<function to_be_decorated at 0x000001DB267E6310>
output to decorate

Decorate That Function Using @

Python has a great syntax to apply a decorator to a function. All you need to do is take the name of the decorator function, add an ‘@‘ symbol to the front of it, and place it on the line directly above the definition of the function to be decorated. In the code below, the function to_be_decorated() is now decorated with the @my_decorator function.

def my_decorator(func):
    '''Decorator Function'''

    def wrapper():
        '''Wrapper Function'''
        result = func()
        return result

    return wrapper


@my_decorator
def to_be_decorated():
    return 'output to decorate'


print(to_be_decorated)
result = to_be_decorated()
print(result)

Up to this point, we have intentionally left the decorator function in a state that does not actually modify the effect of the function that it decorates. Something interesting has occurred however if we run this code above. Let’s see what has changed.

<function my_decorator.<locals>.wrapper at 0x00000211D8096430>
output to decorate

Do you notice what has changed? Before adding the decorator to the to_be_decorated() function, if we simply printed out that function we see:

<function to_be_decorated at 0x000001DB267E6310>

After applying the decorator, printing out to_be_decorated() now shows:

<function my_decorator.<locals>.wrapper at 0x00000211D8096430>

Under The Hood

How did this happen? Well the @my_decorator syntax is a shorthand for this explicit code:

def to_be_decorated():
    return 'output to decorate'


to_be_decorated = my_decorator(to_be_decorated)

Modifying Behavior With A Decorator

We now see how the plumbing works with decorators. Our current decorator is not modifying any behavior for the functions that it decorates. Let’s change that now. Here is an updated version of the my_decorator() function.

def my_decorator(func):
    '''Decorator Function'''

    def wrapper():
        '''Wrapper Function'''
        result = func()
        return result.title().replace(' ', ' !##! ')

    return wrapper

Now we apply that decorator to our function, and notice how the output changed!

@my_decorator
def to_be_decorated():
    return 'output to decorate'


result = to_be_decorated()
print(result)
Output !##! To !##! Decorate

We can apply our decorator to other functions as well.

@my_decorator
def different_func():
    return 'A DIFFERENT FUNCTION'


result = different_func()
print(result)
A !##! Different !##! Function

So why do we use decorators with functions? I mean if I want to change a function why don’t I just go ahead edit the function? Well, consider you are working on a large software project. And say you want to do the same operation to all of the functions like adding logging into the function. Now, this is a big project, so there might be something like 50 different functions. We could go ahead and edit each of the functions. In other words, go into each function, paste some code around logging that function, and then move on to the next function. Alternatively, we could use decorators. The advantage of a decorator is that it will change the behavior of that function without permanently modifying it. So let’s say later on we decide that we don’t want to be logging the functions anymore. It’s easier to simply remove the decorator rather than going into each function and removing some lines of code.


Python Decorator With Arguments

In this section let’s look at using decorators with arguments. To begin, let’s first create a new function. It is a list printer. This function takes a list as a parameter and then transforms that list into a string format suitable for printing.

def list_printer(lst):
    result = '\n'.join(lst)
    return result


lst = ['Harry', 'Bob', 'Alice']

result = list_printer(lst)
print(result)
Harry
Bob
Alice

Now let’s add a new decorator named li_decorator() and apply it to the list_printer() function and try running the code.

def li_decorator(func):
    '''Decorator Function'''

    def wrapper():
        '''Wrapper Function'''
        result = func()
        return result

    return wrapper


@li_decorator
def list_printer(lst):
    result = '\n'.join(lst)
    return result


lst = ['Harry', 'Bob', 'Alice']

result = list_printer(lst)
print(result)
Traceback (most recent call last):
  File "C:\python\decorator.py", line 20, in <module>
    result = list_printer(lst)
TypeError: wrapper() takes 0 positional arguments but 1 was given

Ok, it looks like that didn’t work so well. The reason is that as it is now, the decorator function has no support for arguments. We can fix this by adding the lst argument to the decorator like so.

def li_decorator(func):
    '''Decorator Function'''

    def wrapper(lst):
        '''Wrapper Function'''
        result = func(lst)
        return result

    return wrapper


@li_decorator
def list_printer(lst):
    result = '\n'.join(lst)
    return result


lst = ['Harry', 'Bob', 'Alice']

result = list_printer(lst)
print(result)
Harry
Bob
Alice

*args and **kwargs

The solution above works, but is it the best approach? It turns out, it may not be the best approach. We want our decorators to be flexible so that they can work with a large number of functions to be decorated. If just a single list parameter is hardcoded into the decorator, then the decorator will fail on functions with a different signature. Python provides a nice solution to this problem with the *args and **kwargs keywords. By using both of these in the decorator function, the function is able to be used with any number of positional arguments, keyword arguments, or a combination of both. Here is the updated code using *args and **kwargs.

def li_decorator(func):
    '''Decorator Function'''

    def wrapper(*args, **kwargs):
        '''Wrapper Function'''
        result = func(*args, **kwargs)
        return result

    return wrapper


@li_decorator
def list_printer(lst):
    result = '\n'.join(lst)
    return result


lst = ['Harry', 'Bob', 'Alice']

result = list_printer(lst)
print(result)
Harry
Bob
Alice

Now we will update the decorator function so that it converts the list into an HTML unordered list. This should allow the user to pass a list of strings of any length, and the function will correctly wrap the contents in an HTML unordered list. Here is a quick rendition of that functionality.

def li_decorator(func):
    '''Decorator Function'''

    def wrapper(*args, **kwargs):
        '''Wrapper Function'''
        result = func(*args, **kwargs).split('\n')
        for i in range(len(result)):
            result[i] = f'<li>{result[i]}</li>'
        result = '<ul>\n' + '\n'.join(result) + '\n</ul>'

        return result

    return wrapper


@li_decorator
def list_printer(lst):
    result = '\n'.join(lst)
    return result


lst = ['Harry', 'Bob', 'Alice']
<ul>
<li>Harry</li>
<li>Bob</li>
<li>Alice</li>
</ul>

Calling the function with a different list length also works well. Let’s try again without the decorator, and with the decorator applied while using a longer list of names.

Without Decorator

def list_printer(lst):
    result = '\n'.join(lst)
    return result


lst = ['Susan', 'Christopher', 'John', 'David', 'William']

result = list_printer(lst)
print(result)
Susan
Christopher
John
David
William

With Decorator

@li_decorator
def list_printer(lst):
    result = '\n'.join(lst)
    return result


lst = ['Susan', 'Christopher', 'John', 'David', 'William']

result = list_printer(lst)
print(result)
<ul>
<li>Susan</li>
<li>Christopher</li>
<li>John</li>
<li>David</li>
<li>William</li>
</ul>

Functools Wraps

By replacing a function with another callable, there is some loss of metadata. This might make debugging more tricky. Let’s see what we mean by this in an example. Consider this undecorated function where we print out the dunder name, dunder doc, and help attributes of the function.

def list_printer(lst):
    '''Convert list to string'''
    result = '\n'.join(lst)
    return result

print(list_printer.__name__)
print(list_printer.__doc__)
help(list_printer)
list_printer
Convert list to string
Help on function list_printer in module __main__:

list_printer(lst)
    Convert list to string

You’ve got the name list_printer and we’ve got the docstring for list_printer. Now many editors and the help function use the docstring. So for example, if we typed help and list_printer, we get the docstring for the list_printer function. What happens to this same function when it is decorated? Let’s see.

@li_decorator
def list_printer(lst):
    '''Convert list to string'''
    result = '\n'.join(lst)
    return result

print(list_printer.__name__)
print(list_printer.__doc__)
help(list_printer)
wrapper
Wrapper Function
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)
    Wrapper Function

Now we see a very different result. This time we’re getting the details of the wrapper function. Looking at list_printer dunder doc, we are getting the docstring for the wrapper function. Now, this is because the decorator function li_maker is returning the wrapper. This is an unintended consequence because we want the name and the docstring for our list_printer function. Python provides an easy fix for this with the wraps() module of the functools package.

from functools import wraps


def li_decorator(func):
    '''Decorator Function'''

    @wraps(func)
    def wrapper(*args, **kwargs):
        '''Wrapper Function'''
        result = func(*args, **kwargs).split('\n')
        for i in range(len(result)):
            result[i] = f'<li>{result[i]}</li>'
        result = '<ul>\n' + '\n'.join(result) + '\n</ul>'

        return result

    return wrapper


@li_decorator
def list_printer(lst):
    '''Convert list to string'''
    result = '\n'.join(lst)
    return result


print(list_printer.__name__)
print(list_printer.__doc__)
help(list_printer)
list_printer
Convert list to string
Help on function list_printer in module __main__:

list_printer(lst)
    Convert list to string

You can see that by using wraps from functools, the meta data of the function being decorated is no longer lost. This may help when debugging your code.


Python Class Decorators

Let’s look at an example of how to use decorators with classes in Python. The following example LiDecorator Class provides the same functionality that we saw in our function decorator above. It turns the list_printer() function into an HTML unordered list printer. Let’s examine some of the differences between function decorators and class decorators.

  • First off, we can see that we use the update_wrapper() function instead of @wraps() from the functools module.
  • Next, we see that the name of the class itself is the name of the decorator. So in this case here, LiDecorator is the name of the class, therefore we use @LiDecorator when invoking the decorator.
  • In the __init__ method, we of course accept the self object, but also accept the func callable as an argument.
  • The __call__ method is equivalent to the wrapper() function in a function-based decorator.
from functools import update_wrapper


class LiDecorator:
    def __init__(self, func):
        update_wrapper(self, func)
        self.func = func

    def __call__(self, *args, **kwargs):
        '''Wrapper Function'''
        result = self.func(*args, **kwargs).split('\n')
        for i in range(len(result)):
            result[i] = f'<li>{result[i]}</li>'
        result = '<ul>\n' + '\n'.join(result) + '\n</ul>'

        return result


@LiDecorator
def list_printer(lst):
    '''Convert list to string'''
    result = '\n'.join(lst)
    return result


result = list_printer(['Lisa', 'Bart', 'Maggie'])
print(result)
<ul>
<li>Lisa</li>
<li>Bart</li>
<li>Maggie</li>
</ul>

Popular Python Libraries That Use Decorators

We now have a pretty good understanding of how Decorators in Python are constructed, and what they can be used for. So you might be wondering, what are python decorators good for? Decorators are very popular and are used extensively in well-known Python projects. The most often cited examples are both Flask and Django. For example, you define routes in Flask using decorators. In Django, you have view decorators such as @require_http_methods([“GET”, “POST”]) which determines the allowed HTTP verbs that can be used with a view function. Django also offers a very useful login_required() decorator that can protect any page from unauthenticated users by simply applying a one-line decorator to the view in question. These are great examples of what is the use of decorators.


What Is A Decorator In Python Summary

Now that we understand what decorators are and how they work in Python, now is a great time to experiment with them. There are a few built-in functions in the standard Python library that use decorators. These include the @property decorator, the @staticmethod decorator, and also the @classmethod decorator. Take a look at those decorators and see if you can make sense of how you use them from the documentation. Another good place to brush up on decorators is with the Flask framework since it uses decorators so extensively.

Click to share! ⬇️