Learn Python Decorators from Basic to Pro in 10 mins
Published: 2022-10-01 13:37:04

---
title: Learn Python Decorators from Basic to Pro in 10 mins
published: true
date: 2022-09-11 21:25:12 UTC
tags: 
canonical_url: https://thecodeway.hashnode.dev/learn-python-decorators-from-basic-to-pro-in-10-mins
---

Let's learn about python decorators. This is one of the patterns I love to use in python and It is widely used by senior programmers out there.

# What are decorators 🤔

A decorator in python is a design pattern that allows us to extend the functionality of a function without modifying the function itself. We can do this by wrapping the function we want to extend functionality to with another function. It is not complex at all, it's a very simple pattern. On the low level, we are just passing a function as an argument to another function which will return a function we can call to call the function passed as an argument in the first place.

## Patterns behind a decorator

In python, we can nest functions. Inner functions can access the outer scope of the enclosing function. Meaning that if we define a variable in the enclosed function, the inner function can have access to it. This pattern is called Closure. This concept is very important, it is the core concept of python decorators. This concept is also used in other languages out there, including JavaScript, Golang, and more.

# Coding Decorators

Let's dive into some python codes which will help us understand all those theories and jargon above.

## Functional decorators

Assuming we have a function `calculate_number` that returns a number `5`, but then some requirements came up that we needed to increase the output of that function by `1`, now we can create another function `increase_number` that can help us do that. This is just an example we will cover complex use cases towards the end of the tutorial.

We have our function that returns the number

```
def calculate_number():
    return 5

```

We have another function here, let's pay close attention to this function

```
# Decorator to increase function output by one
def increase_number(func):

    def wrapper():
        """
        Calls the function passed as an argument in the outer
        scope and adds one to the output
        """
        return func() + 1

    return wrapper

```

So let's break this down;

- Take a look at the `increase_number` function, 

- Inside the nested function `wrapper` above, we called the function passed as an argument to the enclosing function and add `1` to it and return the answer,

```
  def outer_function(func):

      print('Before defining inner function')

      def inner_function():

          print('Do something before calling function')

          func() # Call function passed as argument in outer function

          print('Do something after calling function')

      print('Do something before returning the inner function')

      return inner_function

  def sample():
      print('I was decorated')

  outer_function(sample)()

```

Output

```
Before defining inner function
Do something before returning the inner function
Do something before calling function
I was decorated
Do something after calling function

```

Full code: Increasing the number decorator

```
def increase_number(func):

    def wrapper():
        return func() + 1

    return wrapper

def calculate_number():
    return 5

var_a = increase_number(calculate_number)
inner_function_response = var_a()
print(inner_function_response)

```

Running the above code should output `6`.

To summarize what is happening again;

- the function `calculate_number` returns 5,
- the function `increase_number` is a decorator, it takes in a function and wraps it with another function, then returns the wrapper, allowing us to manipulate the return value of the function passed as an argument which is going to be function `calculate_number`.
- the nested function, `wrapper`, inside the function `increase_number` calls the function passed to function `increase_number` and adds 1 to the return value, then returns the answer.
- the nested function, `wrapper`, is returned by the function `increase_number`, so that we can do something with it, most of the time we end up just calling it like a normal function as we did here with `var_a`. I said most of the time because we could also pass the returned function in variable `var_a` as an argument to another decorator, function.

## Pythonic syntax of using decorators

Decorators are used a lot in python, there are even built-in decorators like staticmethod, classmethod, property, and more. So there is a pythonic syntax you can use to decorate a function. Imagine we wanted to apply multiple decorators to the function `calculate_number` in the code above using the current way, it would be too complex for you or another person to understand the code when there is a bug in the code or just reading through the code.

Pythonic Code: Increasing the number

```
def increase_number(func):

    def wrapper():
        return func() + 1

    return wrapper

@increase_number
def calculate_number():
    return 5

print(calculate_number())

```

This is beautiful, I love this syntax. What happened here is simple, we decorated the function `calculate_number` with function `increase_number` by placing it just above the function `calculate_number` with the `@` symbol before it. This tells python that this is a decorator, so when this function `calculate_number` is called, it automatically does the passing of the function to the decorator and calls the wrapper function. So now we can just call the function itself and it will get decorated. Now we can easily add multiple decorators to a function without making it too complex to understand.

## Multiple decorators

Multiple decorators can be applied to a single function, by just stacking the decorators on top of each other using the decorator syntax `@decorator_function`.

```
def split_string(func):
    def inner():
        return func().split()
    return inner

def uppercase_string(func):
    def inner():
        return func().upper()
    return inner

@split_string
@uppercase_string
def speak_python():
    return "I love speaking Python"

print(speak_python())

```

Output

```
['I', 'LOVE', 'SPEAKING', 'PYTHON']

```

The first question that might come to your mind is, what is the order of how this decorator is applied to this function when it is called? The answer is it is from bottom to top, `uppercase_string` then `split_string`. So from the output above you can tell that the string was first converted to uppercase then it was split returning a list of uppercased strings.

You can also test it out, try switching the positions of the decorators, and place `@uppercase_string` on top of `@split_string`.

Code

```
@uppercase_string
@split_string
def speak_python():
    return "I love speaking Python"

```

Output

```
...
AttributeError: 'list' object has no attribute 'upper'

```

From the output above, there was an error because the `@uppercase_string` decorator was trying to call `upper` method on a list. This happened because the `@split_string` decorator split the string first returning a list of strings, then the `@uppercase_string` decorator tried to apply the `upper` method on that list which is impossible as python lists do not have an `upper` method built-in.

## Decorating functions that accept arguments

If our functions accept arguments, no need to worry, we just have to update our decorator to accept the arguments and pass them to the decorated function when calling it. Let's modify our `speak_python` function above to accept a language as an argument, format it with the string, and return it.

Bad Code:

```
@split_string
@uppercase_string
def speak_language(language):
    return f"I love speaking {language}"

print(speak_language("Python"))

```

Output:

```
...
TypeError: inner() takes 0 positional arguments but 1 was given

```

We got the error because the argument `Python` is being passed to the inner functions of our decorators, but those inner functions don't take any arguments, positional or keyword arguments. So we have to update the decorators to make sure the inner functions take in arguments.

Good code:

```
def split_string(func):
    def inner(a):
        return func(a).split()
    return inner

def uppercase_string(func):
    def inner(a):
        return func(a).upper()
    return inner

@split_string
@uppercase_string
def speak_language(language):
    return f"I love speaking {language}"

print(speak_language("Python"))

```

Output:

```
['I', 'LOVE', 'SPEAKING', 'PYTHON']

```

Now it's working, what happened;

- the inner functions now accept an argument,
- then the argument is passed to the decorated function.

## General Decorators

We can also create a decorator that accepts any amount of arguments, positional and keyword arguments, and passes them to the decorated function. Say we updated our `speak_language` function to accept more arguments, we would have to update all our decorators to accept those arguments. Imagine we have a complex codebase with a lot of decorators, it would be hard to update all of them. So we should make sure our decorators can handle multiple arguments in the future.

Best Code:

```
def split_string(func):
    def inner(*args, **kwargs):
        return func(*args, **kwargs).split()
    return inner

def uppercase_string(func):
    def inner(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return inner

@split_string
@uppercase_string
def speak_language(word, language):
    return f"I {word} speaking {language}"

print(speak_language("hate", "Python"))

```

Output:

```
['I', 'HATE', 'SPEAKING', 'PYTHON']

```

_args and \*_kwargs allow you to pass multiple arguments or keyword arguments to a function. So we used it in the inner functions to accept any number of arguments and also pass them all to the decorated function. Now no matter how many arguments are passed to the `speak_language` function in the future, we don't need to update the decorators. One less problem to think about. 😁

[Learn more about _args and \*_kwargs](https://realpython.com/python-kwargs-and-args)

## Decorator Factory

Let's go back to our increase\_number function Code:

```
def increase_number(func):

    def wrapper():
        return func() + 1

    return wrapper

@increase_number
def calculate_number():
    return 5

print(calculate_number())

```

This decorator only increases this number by `1`, what if we want to apply an increase by 10? We can edit the decorator to increase it by 10. What if we have another function that needs a decorator to increase it by 5, we can create another decorator like `increase_number` that increases it by 5. This is not good because we are repeating code, we should try not to repeat codes anywhere possible because if there turns out to be a bug in the code we will have to change all places where the code has been replicated.

It is also possible with decorators. With the understanding of Closures, we know that a nested function has access to the scope of the outer function. Then we can create a function that returns a decorator. Doing this means if we pass an argument to the function creating the decorator, the decorator would have access to the arguments passed to its outer function and the inner function of the decorator should have access to these arguments too. Let's take a look at the code below.

Code:

```
def A(arg1):
    def B():
        def C():
            def D():
                def E():
                    print(arg1)
                return E
            return D
        return C
    return B

A("Good Python")()()()()

```

Output:

```
Good Python

```

I think with this piece of code, I have done justice to the fact that nested functions always have access to the outer function scope. Function `E` was nested four levels down, but still has access to the scope of function `A`.

This is the same idea behind a decorator factory, a function that returns a decorator. So to make the applied increase dynamic, we can create a function that accepts the number to increase by as an argument and then return a decorator which has access to this number. Code:

```
def apply_increase(increase):

    def increase_number(func):

        def wrapper():
            return func() + increase

        return wrapper

    return increase_number

@apply_increase(10)
def calculate_number():
    return 5

@apply_increase(1)
def myother_number():
    return 1

print(calculate_number())
print(myother_number())

```

Output:

```
15
2

```

The `apply_increase` function accepts the number as an argument, then returns the decorator. Since the decorator function returned for each of the functions above has access to the scope of their outer functions, we get unique decorators because the numbers in `apply_increase(10)` and `apply_increase(1)` are different. For both of the decorators returned, their `increase` argument will be different.

## Conclusion 👍

Decorator is a very amazing pattern in python, with it we can extend the functionality of the wrapped function. We have covered a lot in this article, but there are other things we haven't covered. Class decorators, method decorators, and python functools module will be covered in another article. Thanks for reading, Arigato.

If you love this article, you can give this article likes and 10 reactions, comment below if you have any questions or views, and follow me for more updates on Software programming.

![KissThankYouGIF.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1662930969832/HyR_7ryqw.gif)