Understanding python decorators with step by step examples
Tags: python, beginners, computerscience
Published: 2020-10-30 15:52:02

---
title: Understanding python decorators with step by step examples
published: true
description:  Understanding decorators is tricky. Here's your step by step guide to understand how decorators work! 
tags: python, beginners, computerscience
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/64pjtvp4g4dtx3ka2wbo.png
canonical_url: https://bharatkalluri.com/blog/decorators-python/
---


Decorators are used to add functionality to an existing code. They allow you to wrap a function within another function, thereby adding functionality.
Let us look at some examples to get a sense of what decorators are. Before that, let us get some concepts cleared out about functions in python

## Functions are first class objects in python
A **function can be used like a value in python**. For example
```python
def printer():
    print("Printing")
a = printer
a()
# printing
```
Here we assigned the function to a variable and now the variable holds the function. 
**Functions can also be passed around as arguments between functions** (since they just act like values). Another popular example is `map`. The first argument of the function is a function (`Callable`) and the second argument of the function is an `Iterable`(List, tuple etc.). Then `map` runs over the `Iterable` and executes the function. 
Let us write our own simple `map` implementation
```python
from typing import Callable, Iterable

def mapper(func: Callable, list_to_iterate: Iterable):
    return [func(el) for el in list_to_iterate]

def double(number: int):
    return number * 2
mapper(double, [1,2])
# [2, 4]
```

Functions can also be nested inside one another. For example
```python
def outer_func():
    def inner_func():
        print("Inner func")
    print("Outer func")
outer_func()
# Outer func
# Inner func
```

Armed with this information, let us create our first decorator

## A simple decorator
Let us create a function that counts the number of seconds a function took to execute. A very similar example to the `mapper`function.
```python
from time import time, sleep
from typing import Callable

def timer_decorator(func: Callable) -> Callable:
    def wrapper():
        start_time = time()
    	func()
	    end_time = time()
    	print(end_time-start_time)
    return wrapper

def sleeper() -> None:
    sleep(5)

timed_sleeper: Callable = timer_decorator(sleeper)

timed_sleeper()
# 5.00
```

The above example pretty much combines everything we have learned up until now. The `timer_decorator`returns a wrapper function. The function starts up a timer, runs the input function, and then ends the timer and calculates the difference to print it out. `sleeper`just sleeps for 5 seconds.
When `timer_decorator` is called with `sleeper` as an argument. We get back a function which wraps the sleeper func with the time difference calculation logic. Now, if `timed_sleep`is called, it runs and prints out the number of seconds it took to run the `sleeper`func.
Takes some time and make sure you understand what is happening. This is what decorators are. Python gives us some syntactic sugar over the same idea. Using decorators syntax, the sleeper function would look like this
```python
@timer_decorator
def sleeper():
    sleep(5)

sleeper()
# 5.00
```

That is it! **Decorators wrap a function and modify its functionality/behaviour**

## Decorating functions with arguments
The current code does not work well for functions with arguments, let us see why
```python
from time import time, sleep
from typing import Callable

def timer_decorator(func: Callable) -> Callable:
    def wrapper():
        start_time = time()
    	func()
	    end_time = time()
    	print(end_time-start_time)
    return wrapper

@timer_decorator
def sleeper(sec: int):
    sleep(sec)
    
sleeper(5)

# ---> 16 sleeper(5)

# TypeError: wrapper() takes 0 positional arguments but 1 was given
```
The error is pretty clear, wrapper does not take any arguments. So now we should have a mechanism to cascade arguments to child functions. Python's `*args` and `**kwargs`come in handy here. Let us modify the decorator so that it cascades all arguments to child functions.
```python
from time import time, sleep
from typing import Callable

def timer_decorator(func: Callable) -> Callable:
    def wrapper(*args, **kwargs):
        start_time = time()
    	func(*args, **kwargs)
	    end_time = time()
    	print(end_time-start_time)
    return wrapper

@timer_decorator
def sleeper(sec: int):
    sleep(sec)
    
sleeper(5)
# 5.00
```
And, it works as expected!

## Returning values from decorated functions
Give it a thought, what would happen if sleeper were to return the number of seconds it was sleeping. Who would have the return value? 
According to the current implementation, the decorator is not giving back anything. We just call the `func`but are not returning anything from the wrapper function. The returns also need to cascade upwards so that the caller get back the expected value.
```python
from time import time, sleep
from typing import Callable

def timer_decorator(func: Callable) -> Callable:
    def wrapper(*args, **kwargs):
        start_time = time()
    	ret_val = func(*args, **kwargs)
	    end_time = time()
    	print(end_time-start_time)
        return ret_val
    return wrapper

@timer_decorator
def sleeper(sec: int):
    sleep(sec)
    return sec
    
sleep_time = sleeper(5)
print(sleep_time)
# 5.00 (This print is from the decorator)
# 5 (This is from the print statement)
```

## Passing arguments to decorators
What if we want to have a `timer_decorator`that takes in an argument, say `in_min`which can be true or false. And if true, then the time should be printed in minutes instead of seconds. 
Remember that the only requirement from a decorator is that it should return a function that takes in a function as an argument. 
```python
from time import time, sleep
from typing import Callable


def timer_decorator(in_min: bool = False):
    def decorator(func: Callable) -> Callable:
        def wrapper(*args, **kwargs):
            start_time = time()
            ret_val = func(*args, **kwargs)
            end_time = time()
            time_consumed = end_time - start_time
            print(time_consumed if in_min is False else time_consumed / 60)
            return ret_val
        return wrapper
    return decorator


@timer_decorator(in_min=True)
def sleeper(sec: int):
    sleep(sec)
    return sec


sleep_time = sleeper(5)
# 0.08336979548136393 (time consumed in minutes)
```
In this implementation, `timer_decorator`takes in an argument and gives back a decorator (!). This decorator already has access to the variable `in_min` and will use it. This might be the most complicated example of the post. Take your time and make sure you understand everything clearly. 

## Practical use cases of decorators
- **Flask uses decorators for routing, authentication guards etc..**:  It makes the code much more clean and readable
- **Decorators can be used for profiling**: Like the example we just wrote
- **Retry decorator on certian exceptions**: An `@retry`decorator which takes in a list of exception types as arguments and retry's the function if the function exceptions out (Celery does this)

And many more...
	
Congratulations on coming this far! Decorators are quite tricky in the beggining, but once you understand them. They are really powerful and useful.

## Further Reading
- [The decorator proposal](https://www.python.org/dev/peps/pep-0318/)
- [Some interesting decorator examples](https://wiki.python.org/moin/PythonDecoratorLibrary)