Using Decorators in Python:

annu mishra
7 min readFeb 21, 2021

Introduction

The simplest and fastest way to distinguish junior and senior Python programmers in a technical interview is letting him or her write a decorator. Back then in 2019 , I was rejected from a interview because I didn’t had depth knowledge on decorators. Because mastering the decorator, which is the most magical Python feature of all time, is a milestone for a Python developer.

There are many tips and tricks worth to mention about decorators, but they are scattered around different books or tutorials and some of them may make beginners more confused. This is why I wrote this article.

This article will dive into all the core concepts, techniques and usages of Python decorators by 7 levels. If you understand half of them, reading Python programs containing decorators will be easy. If you understand all of them, designing and writing decorators in Python will be just a piece of cake. 🍰

Level 0: Understand the Basic Concepts and Usages

What is a decorator?

The decorator is just another functional programming feature in Python. It receives a callable (function, method or class), and returns a callable. Let’s see a simple example:

The above add_author(func) function is a simple decorator. It prints the author’s name before the received function runs.

If a function needs to use this decorator, we can add it following an “@” sign on the top of the function.

The use of the “@” sign looks confusing at first, but it’s just a syntax sugar of Python. We can apply the decorator as following as well:

Results of the above method are totally the same as the “@” way:

Therefore, the “@” method is not intimidating at all. It just gives us a very intuitive and elegant option to apply a decorator to a function.

Why do we need decorators?

A decorator works like a reusable building block, we can apply it to a function when it’s needed without editing the function itself.

As the previous example shown, whenever we need to print the author name before a get_title() function, we can just assemble this block (the add_author decorator) into the function. No modification within the get_title() function is needed. If we don’t need to print the author name in future, just delete or comment out the one line on the top of get_title().

Editing a function many times is bug-prone. Assembling decorators when it’s needed is elegant and bug-free.

In a nutshell, the decorator gives us lots of flexibility and decouples the functionality methods and main methods. This great feature is used in many Python built-in modules and popular third-party modules.

Level 1: Wrap a Function Properly

A decorator is also called a wrapper in some materials. Because it can wrap a function and change its behaviours.

As to the example in level 0, our decorator just printed something before the get_title() executed. Can we do more? Such as changing the returned title of get_title()?

Yes, of course, if we know how to wrap the function as follows:

As demonstrated above, we define an inner function called wrapper(), which wraps the received func and adds three exclamation marks at the end of its result.

Basically, this example shows a common template for writing a decorator in Python. There are 3 steps:

  • Receive a function as an argument
  • Define a wrapper function which will do something with the received function
  • Return the wrapper function

By the way, in the context of functional programming, our decorators which contain nested functions are named closures.

So far, we have already known the fundamentals of decorators. We are able to write some simple decorators as well. 🎉

Unfortunately, actual requirements can be very complicated and the above basics are not enough to design a robust decorator. The next levels will introduce more advanced techniques of decorators.

Level 2: Apply Multiple Decorators to a Function

Since a decorator is used as a functionality block, sometimes we would like to assemble many decorators to one function. How can we implement it?

Very easy, just put all the needed decorators on the top of the function as follows:

One important thing we should mind is the order of the used decorators. If we change the above example’s order, the results are different:

In fact, multiple decorators will wrap the function layer by layer from the bottom to the top. The above code is identical with the following:

Level 3: Wrap a Function That Receives Arguments

Our previous example program is good but not flexible enough. If we add an argument to the get_title() function to let it receive a string as the title, it will be better.

But how to modify the decorators to adapt this change?

We can just let the wrapper function help us receive the argument:

The above code has already solved the problem, but it’s not very robust.

As mentioned previously, a decorator is a building block and can be added to other functions when it’s needed. However, we cannot make sure that all functions assembling the add_author decorator have only one argument.

Therefore, our decorator is limited and cannot be applied to functions including many arguments.

Do we have to write many similar decorators that just have different arguments in the wrappers?

Fortunately, we don’t have to do that. The asterisks technique can make our lives easier:

As shown above, with the help of asterisks, our decorator can be assembled to functions without caring about how many arguments they will receive.

This is a popular and recommended way to design a decorator, since it makes a decorator more flexible and powerful.

Level 4: Write a Decorator That Receives Arguments

In fact, the examples in the previous level have another obvious bug:

Annu Mishra is not the author of “Harry Potter”!

We should let our decorator more flexible so it can receive an argument which represents the real author’s name of “Harry Potter”.

Now, things become a little complex: both the target functions and the decorator itself should receive arguments. The idea to implement this task is adding another layer outside the existed decorator.

As stated above, we just need to add one outer layer to the add_author decorator to receive the argument.

The above program is identical with the following:

Level 5: Keep the Metadata of Original Functions

Until now, we have already designed a very flexible and robust decorator! But a true python developer will consider all details. Actually, the decorator function has a hidden side effect. Let’s see it by the following example:

The above results are not as expected. The name and doc of the get_title function have been wrapped as well! This is the side effect of decorators.

To avoid this side effect, we can write some code like wrapper.__name__ = get_title.__name__ manually. But there is an easier way:

As shown above, we can use the wraps decorator in the functools module, which will help us protect the metadata. As far as I am concerned, adding the wraps decorator to every wrapper function is a good practice to avoid unexpected results.

Level 6: Keep It Simple — The Design Philosophy of Decorators

Congratulations! 🎉 🎉

If you arrive at this level, you have already understood, at least known, all the core techniques of Python decorators.

Last but not least, there is one philosophy worth to mention before you start to design decorators for your projects:

Keep it simple and stupid.

The decorator is an elegant tool to help us write clean and neat Python code. But don’t overuse it or write a too complicated decorator. In my opinion, a decorator which has three layers of functions is enough, and assembling three decorators into a function is also enough.

As an old saying goes, beyond is as wrong as falling short. We should always mind the readability of our code and keep everything simple, even if we are using a complex feature.

Thanks for reading. Please comment your own ways .

--

--