Decorators in Python

Eid Araache
5 min readJun 25, 2023

--

Before diving into Python and how to write a decorator, let`s answer the following questions:

  1. What is a decorator?
  2. Why/When to use a decorator?

Let`s imagine that you have a TV and at some point in time, you are no longer happy with its sound system. In such a situation, you have at least two options, either buying a new TV or buying an external sound system as an attachment.
If you decide to go with the second option (buying an external sound system), you are attaching a decorator to your TV.

A decorator is a design pattern that is used to extend/modify the functionality of already existing functions/modules.

Let`s go back to our example and ask the question, why should we take the second option?

  • Most likely the sound system cost is much lower than the cost of buying a new TV with a good sound system. The same goes for code, adding an attachment to modify a function/module behavior is much “less effort” than re-write the functionality.
    In some cases re-writing the functionality can be less expensive for long-term run because the code could be using some outdated technologies that require high maintenance effort.
  • The external sound system can be easily moved around and connected to different devices. With a decorator, it is just “plug and play”, you can apply the same additional functionality to multiple functions.
  • You have some old device that needs an outdated port to connect to and currently, it works well with your old TV so with buying a new TV, you will need to find some workaround to support that device. Your code is used by others, and changing it directly would cause integration problems and you might need to consider a backward-compatible form.

How to write a decorator in Python

Let`s imagine NASA has sent a robot to the Moon to take some measurements. The robot has all different kinds of sensors and one of them is a temperature sensor. The temperature sensor measures the temperature in Fahrenheit and because of some new regulation, they need to have the temperature in Celsius. Of course, they can not change the sensor because it will cost much more than just building and sending a new robot. So let`s try to solve it programmatically.

For simplicity, We will assume that we have a class called TemperatureSensor, which has a function called read_sensor_fahrenheit that returns the temperature.

class TemperatureSensor:
def read_sensor_fahrenheit(self):
return 300

We have also a function called print_temperature that prints the value returned by the sensor. The output of that function can be used by different devices and applications.

def print_temperature():
senor = TemperatureSensor()
return senor.read_sensor_fahrenheit()

Now, we will write a decorator to modify the behavior of print_temperature and print the temperature in Celsius.

from typing import Callable
def to_celsius(function: Callable)-> Callable:
def inner():
temp_in_fahrenheit = function()
return (temp_in_fahrenheit - 32) * 5/9
return inner

A decorator is defined in Python as a function that takes a Callable (Function) as a parameter and also returns a Callable. We have an inner function that calls the Callable to get the original output of the function that we want to modify and apply the new behavior.

Now let`s apply the decorator to our print function. It can be easily done by adding the Symbol “@” followed by the name of the decorator directly before the definition of the function to which we want to apply the decorator.

@to_celsius
def print_temperature():
senor = TemperatureSensor()
return senor.read_sensor_fahrenheit()

For a better understanding, let`s see how the decorated function is interpreted.

print_temperatue = to_celsius(print_temperatue)

So as we can see the print_temperatue has been redefined as the output of to_celsius function taking the original print_temperatue as a parameter.

Let`s make the study case a bit complicated and assume that multiple institutions will be using the output provided by that sensor and they want the output in different temperature units. Still, our decorator can do the magic and give different outputs by applying a small modification.

from typing import Callable
def convertor(arg1):
def decorator(function: Callable)-> Callable:
def inner():
temp_in_fahrenheit = function()

if arg1 == "C":
return (temp_in_fahrenheit - 32) * 5/9

elif arg1 == "K":
return (temp_in_fahrenheit - 32) * 5/9 + 273.15

return temp_in_fahrenheit

return inner
return decorator

What we have done is just wrapping the decorator by another function that takes one parameter which holds the value that is passed to the decorator annotation. The print_temperatue will be decorated like the following when we want the output to be in Kelven:

@convertor('K')
def print_temperature():
senor = TemperatureSensor()
return senor.read_sensor_fahrenheit()

And the interpretation will be like this:

print_temperatue = convertor('K')(print_temperatue)

What if we like to add a small sentence before the output of the print_temperatue saying “The Temperature on Moon is”. How can we do that?

Right, we can create another decorator.

from typing import Callable

def add_prefix(function: Callable)-> Callable:
def inner():
temperature = function()
return f"The Temperature on Moon is {temperature}"

return inner

Now, we can apply the new decorator like the following:

@add_prefix
@convertor('K')
def print_temperature():
senor = TemperatureSensor()
return senor.read_sensor_fahrenheit()

Multiple decorators can be applied on top of each other creating a chain of function calls that is interpreted as:

print_temperatue = add_prefix(convertor('K')(print_temperatue))

Decorator as a class

Defining a decorator as a function is not the only way to do it in Python but it can be also defined as a class with __call__ method that implements the new behavior. The class attributes hold the values passed to the decorator annotation.

from typing import Callable

class Convertor:
def __init__(self,temp_unit: str):
self.temp_unit = temp_unit

def __call__(self,function: Callable):
def inner():
temp_in_fahrenheit = function()

if self.temp_unit == "C":
return (temp_in_fahrenheit - 32) * 5/9

elif self.temp_unit == "K":
return (temp_in_fahrenheit - 32) * 5/9 + 273.15

return temp_in_fahrenheit

return inner

@add_prefix
@Convertor('K')
def print_temperature():
senor = TemperatureSensor()
return senor.read_sensor_fahrenheit()

Decorators are widely used in Python and almost every framework and library has its own defined decorators. For example:

  • FastAPI uses decorators to define the endpoint of a web application.
  • Pytest has a set of built-in decorators to support the testing process such as fixtures
  • Static methods in Python are defined using the decorator concepts.
  • … etc

--

--

Eid Araache
Eid Araache

No responses yet