How Python Decorators Work: 7 Things You Must Know

A Python decorator is a feature (or design pattern) that allows to enhance the logic of functions, methods or classes without changing the original code. To represent a decorator in Python you can use the @ symbol followed by the name of the decorator.

In this tutorial, we will go through 7 things you must know about Python decorators.

We will start with a simple example and then we will keep building on it to make the learning process a lot easier.

Get Started with a First Decorator Example

To understand how Python decorators work we will start with a simple function called print_message():

def print_message():
    print("Original message")

A decorator takes a function as input, adds some functionality to it, and returns the function.

What does it mean?

In other words, a decorator provides additional functionality to existing code (e.g. a function) without changing the original code.

But, how does it do it without changing the original code?

Here’s how…

We will create another function called print_additional_message(). This function takes as an argument another function called func.

Inside this function we will define another function called wrapper() that does the following:

  1. Print another message.
  2. Call the function func() that as mentioned before is passed as an argument.

Finally, the last line of the print_additional_message function returns the wrapper function…

…here is the code:

def print_additional_message(func):
    def wrapper():
        print("Decorator message")
        func()
    return wrapper

We call it wrapper function because this function is a wrapper around the original function. In other words, it can perform operations before and after calling the original function.

To decorate a function you can use the @ symbol followed by the name of the decorator function above the definition of the function to be decorated.

Here is how we can apply the print_additional_message decorator to the print_message() function:

@print_additional_message
def print_message():
    print("Original message")

Below you can see the full code:

def print_additional_message(func):
    def decorator():
        print("Decorator message")
        func()
    return decorator

@print_additional_message
def print_message():
    print("Original message")

print_message()

And the output when we execute our program is…

Decorator message
Original message

So, as you can see the first message comes from the decorator, and the second message from the print_message() function.

Now, let’s move to something more complex…

How to Use a Python Decorator With a Function That Takes Arguments

Let’s dig deeper into the concept of decorators…

In this example, we will look at how to use Python decorators to increase the security of your code.

Firstly, I will define a list of numbers and a function called update_list() that updates the elements of my list by appending an additional element.

def update_list(original_list, new_element):
    original_list.append(new_element)
    return original_list

numbers = [1, 2, 3]
print(update_list(numbers,5))

Before continuing verify that this code works as expected. The output should be:

[1, 2, 3, 5]

Now, let’s say this function is part of a bigger system and I want to make sure only logged-in users can update this list.

How can I do it with decorators?

Define a dictionary called user. The attribute logged_in tells us if the user is logged into our system or not.

user = {'name': 'codefathertech', 'logged_in': False}

Then we can write the verify_user() function that will be used for our decorator.

This function takes as argument another function that we will call func. Also inside this function, we will define another function called wrapper.

Do you remember?

This is a similar approach to the one we have used in the previous example:

def verify_user(func):
    def wrapper(original_list, new_element):
        ....
        ....

Notice how the wrapper function takes as arguments the same arguments of our original function update_list().

Inside the wrapper function, we verify if the user is logged in or not:

  • If the user is not logged in we print an error message and we return from the function.
  • Otherwise, we return the original function

And finally, inside the verify_user() function we return the wrapper function object.

def verify_user(func):
    def wrapper(original_list, new_element):
        if not user['logged_in']:
            print("User {} is not logged in!".format(user['name']))
            return

        return func(original_list, new_element)
    return wrapper

The wrapper function is nested inside the decorator function. This is one of the features of Python that allows to nest functions inside other functions.

To apply the decorator to our update_list() function we use the @ sign followed by the name of the decorator just above the method definition.

The full code at this point is:

def verify_user(func):
    def wrapper(original_list, new_element):
        if not user['logged_in']:
            print("User {} is not logged in!".format(user['name']))
            return

        return func(original_list, new_element)
    return wrapper

@verify_user
def update_list(original_list, new_element):
    original_list.append(new_element)
    return original_list

numbers = [1, 2, 3]
user = {'name': 'codefathertech', 'logged_in': False}
print(update_list(numbers,5))

Let’s find out if this decorator works!

The logged_in attribute for the user is False and the output we get when we run the program is:

User codefathertech is not logged in!
None

Good, the decorator prevents the user from updating the list.

If we set logged_in to True:

user = {'name': 'codefathertech', 'logged_in': True}

Our program allows the user to modify the list.

Adding a New Argument to a Decorated Function

Let’s improve the code of our decorator to give more details to our users.

If the user is not logged in we print an ERROR message, if the user is logged in we print an INFO message. This can be very useful considering that often applications print hundreds of thousands of messages…

…so the more the details, the better.

The verify_user() function becomes:

def verify_user(func):
    def wrapper(original_list, new_element):
        if not user['logged_in']:
            print("ERROR: User {} is not logged in!".format(user['name']))
            return
        else:
            print("INFO: User {} is logged in".format(user['name']))
            return func(original_list, new_element)

    return wrapper

Now let’s see what happens if we add a new argument to the function update_list().

The function will also add this new argument to our list.

First of all, we will test our function after commenting the decorator. In this way we can confirm the function works fine:

#@verify_user
def update_list(original_list, new_element, additional_element):
    original_list.append(new_element)
    original_list.append(additional_element)
    return original_list

numbers = [1, 2, 3]
print(update_list(numbers,5, 7))

Action: make sure the output matches the following:

[1, 2, 3, 5, 7]

This code works fine without the decorator but when we enable the decorator and rerun the code, we get an error:

Traceback (most recent call last):
   File "/opt/python/codefathertech/decorators_tutorial.py", line 49, in 
     print(update_list(numbers,5, 7))
 TypeError: wrapper() takes 2 positional arguments but 3 were given

This error is caused by the fact that in the definition of the wrapper function, we haven’t included the new argument.

So, we will add the new argument to the definition of the wrapper function and also to the return statement in the else branch of the wrapper function.

Here’s how the verify_user() decorator becomes (no other changes to our code):

def verify_user(func):
    def wrapper(original_list, new_element, additional_element):
        if not user['logged_in']:
            print("ERROR: User {} is not logged in!".format(user['name']))
            return
        else:
            print("INFO: User {} is logged in".format(user['name']))
            return func(original_list, new_element, additional_element)

    return wrapper

Action: Verify that the decorated method works fine for both values of the logged_in attribute, True and False.

Python Decorator Using args And kwargs

Even if the code in the previous section works, this is not an ideal way of handling arguments.

Imagine if we had to add multiple arguments to the update_list() function. Every time we have to do that we also need to update the wrapper function in two places.

Can we handle this in a better way?

Instead of passing exact names for the arguments of the wrapper function, we can pass two arguments that are used in Python to provide an arbitrary number of positional arguments or keyword arguments: args and kwargs.

Args is used in Python to pass an arbitrary number of positional arguments to a function (written as *args). Kwargs allows to pass an arbitrary number of keyword arguments to a function (written as *kwargs).

We will use *args and **kwargs in two places:

  • In the definition of the wrapper function.
  • When we return the function we are decorating inside the wrapper function.

Our decorator becomes…

def verify_user(func):
    def wrapper(*args, **kwargs):
        if not user['logged_in']:
            print("ERROR: User {} is not logged in!".format(user['name']))
            return
        else:
            print("INFO: User {} is logged in".format(user['name']))
            return func(*args, **kwargs)

    return wrapper

Notice the two places in which *args and **kwargs are used.

To make sure it’s clear how args and kwargs work, we will print the positional arguments (*args) and keyword arguments (**kwargs) at the beginning of the wrapper function.

def verify_user(func):
    def wrapper(*args, **kwargs):
        print("Positional arguments:", args)
        print("Keyword arguments:", kwargs)

        if not user['logged_in']:
            print("ERROR: User {} is not logged in!".format(user['name']))
            return
        else:
            print("INFO: User {} is logged in".format(user['name']))
            return func(*args, **kwargs)

    return wrapper

When we execute the code in the same way we have done before…

print(update_list(numbers,5, 7))

We only see positional arguments in the output because we are not passing any keyword arguments (make sure logged_in is True:

Positional arguments: ([1, 2, 3], 5, 7)
Keyword arguments: {}

Let’s update the call to the update_list() function to pass keyword arguments instead:

print(update_list(original_list=numbers, new_element=5, additional_element=7))

The output changes:

Positional arguments: ()
Keyword arguments: {'original_list': [1, 2, 3], 'new_element': 5, 'additional_element': 7}

This time there are no positional arguments and we can see the keywords arguments passed to the function.

How to Define a Python Decorator With Arguments

Now I want to show you how you can pass an argument to a decorator.

But, why would you do that?

Let’s say your application has multiple modules and you want to know which module is logging a specific message.

We can do that by passing an application_module to the decorator and then using that value when we print an ERROR or INFO message.

In this way when we look at our logs we know immediately which application module has logged a specific message.

Here is how we want to use our decorator:

@verify_user('SecurityModule')

To pass an argument to our decorator we need to add another level of nesting to the code of our decorator. We basically add another level of function that returns our decorator.

Don’t forget the additional return statement at the end of the verify_user() decorator function.

Here is the new implementation of the decorator:

def verify_user(application_module):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print("Positional arguments:", args)
            print("Keyword arguments:", kwargs)

            if not user['logged_in']:
                print(application_module, "ERROR: User {} is not logged in!".format(user['name']))
                return
            else:
                print(application_module, "INFO: User {} is logged in".format(user['name']))
                return func(*args, **kwargs)

        return wrapper
    return decorator

At this point, we can also pass the application_module to the two print statements inside the if else statement of the wrapper function.

This is the output we get when we execute our code and logged_in is True:

SecurityModule INFO: User codefathertech is logged in
[1, 2, 3, 5, 7]

Here is the full code:

def verify_user(application_module):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print("Positional arguments:", args)
            print("Keyword arguments:", kwargs)

            if not user['logged_in']:
                print(application_module, "ERROR: User {} is not logged in!".format(user['name']))
                return
            else:
                print(application_module, "INFO: User {} is logged in".format(user['name']))
                return func(*args, **kwargs)

        return wrapper
    return decorator

@verify_user('SecurityModule')
def update_list(original_list, new_element, additional_element):
    original_list.append(new_element)
    original_list.append(additional_element)
    return original_list

numbers = [1, 2, 3]
user = {'name': 'codefathertech', 'logged_in': False}
print(update_list(original_list=numbers, new_element=5, additional_element=7))

Action: test this code also when logged_in is False.

Improve Your Python Decorator With The Functools Wraps Function

Before completing this tutorial I want to show you a common problem that occurs with decorators.

It’s something that can make troubleshooting your programs harder for you and for those who use the Python modules you write.

Let’s start with the code at the end of the last section…

We will add a docstring to the update_list() function and to the wrapper() function.

We will also add two print statements to print the name and the docstring for the function passed to the wrapper function.

def verify_user(application_module):
    def decorator(func):
        def wrapper(*args, **kwargs):
            """Wrapper function for verify_user decorator"""
            print("The name of the function called is", func.__name__)
            print("The docstring of the function called is", func.__doc__)
            ...
            ...
        return wrapper
    return decorator

@verify_user('SecurityModule')
def update_list(original_list, new_element, additional_element):
    """Add two elements to a list"""
    original_list.append(new_element)
    original_list.append(additional_element)
    return original_list

When you run the code you will see the following messages:

The name of the function called is update_list
The docstring of the function called is Add two elements to a list

So, the name and docstring of the update_list() function are visible inside the wrapper function.

Now, let’s print function name and docstring for update_list() after its definition:

@verify_user('SecurityModule')
def update_list(original_list, new_element, additional_element):
    """Add two elements to a list"""
    original_list.append(new_element)
    original_list.append(additional_element)
    return original_list

print("The name of the function called is", update_list.__name__)
print("The docstring of the function called is", update_list.__doc__)

Something weird happens, look at the output…

The name of the function called is wrapper
The docstring of the function called is Wrapper function for verify_user decorator

The wrapper function in our decorator hides the metadata of the decorated function.

To solve this problem we can use the wraps function of the functools module.

Functools.wraps is a function decorator that preserves the metadata of a decorated function.

Let’s see how it works…

from functools import wraps

def verify_user(application_module):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ...
            ...
        return wrapper
    return decorator

There are only two changes to our code:

  1. Import wraps from the functools module.
  2. Decorate the wrapper function with @wraps(func).

This time when you run the program you get the right info back:

The name of the function called is update_list
The docstring of the function called is Add two elements to a list

Makes sense?

How to Deepen Your Decorators Knowledge

One of the best ways to deepen your Python knowledge and in this case, your decorators’ knowledge is by looking at code used in Python frameworks.

The example below comes from the Django framework. I have removed the implementation of the _wrapped_view() function so you can focus on the structure of the decorator.

def make_middleware_decorator(middleware_class):
    def _make_decorator(*m_args, **m_kwargs):
        def _decorator(view_func):
            middleware = middleware_class(view_func, *m_args, **m_kwargs)

            @wraps(view_func)
            def _wrapped_view(request, *args, **kwargs):
                ...
                ...
            return _wrapped_view
        return _decorator
    return _make_decorator

Can you see some of the concepts we have covered in this tutorial?

In this code we can see the following:

  • Multiple levels of nested functions that as explained before are at the core of the decorators.
  • A wrapper function called _wrapped_view.
  • The wrapper function takes as arguments *args and **kwargs.
  • @wraps(view_func) decorates the wrapper function.
  • Return statements at each nesting level.

Do you see how much easier is to understand this code now?

Conclusion

To recap, in this tutorial, we have seen how to:

  1. Define a simple function decorator to add extra functionality before and after the function that gets decorated.
  2. Apply a decorator to a function that takes one or more arguments.
  3. Add a new argument to an existing decorated function.
  4. Use *args and **kwargs to define a flexible decorator function that doesn’t need changing even if the number of arguments passed to the decorated function changes.
  5. Pass an argument to a decorator.
  6. Decorate the wrapper function with functools.wraps() to preserve the metadata of the original decorated function.
  7. Deepen your decorators knowledge by looking at decorators in other projects (e.g. the Django framework).

I understand that the syntax of decorators can be quite tricky to remember, especially if you are just getting started with them.

I suggest going through this code again and try writing this code by yourself from scratch. This will help in the future when you will have to write a decorator or even if you have to understand a decorator written by someone else.

Congratulations for getting to the end of this tutorial and let me know in the comments if there is anything else you would like to learn about decorators.

1 thought on “How Python Decorators Work: 7 Things You Must Know”

  1. Very well written and explained.
    The way the article builds up, step by step, from the very basic to the more specific scenarios and finally reaching the actual full fledged example is really excellent!

    The the section “How to Deepen Your Decorators Knowledge” at the end, actually makes this article stand out. This is what most of the tutorials miss.

    Thank You.

    Reply

Leave a Comment