Python Decorators - Deep Dive in 2 Parts (1/2)
- Valeria Aynbinder
- Coding
- 12 Jul, 2024
This is a 2-Part Series:
- Part 1 (current): Intro + Build Your First Decorator
- Part 2: Decorators for functions that receive parameters and return values, decorators that receive arguments, decorators in classes + advanced exercises with solutions
A decorator is a special feature in Python that lets you modify the behavior of existing functions without changing their code. Yes, you read that right — you can change functions without actually altering their code! This is what makes decorators so powerful and useful in Python. In this article, we will explore what decorators are and how you can use them to make your code cleaner and more efficient.
By the end of this guide, you'll know what a decorator is, how it works, and how to make your own. Along the way, you might learn some new things about functions too! 😊 Decorators are often used for things like logging, checking permissions, or adding functionality in a clean way. Let’s get started!
Watch my video about Python Decorators with code examples for better understanding.
Motivating Task
Let’s start with an example that shows why we need decorators.
Imagine you have a Python module called various_functions.py that includes several useful functions like calculating a factorial, capitalizing a name, and converting temperatures. Each of these functions takes input from a user, performs its calculations, and prints the result:
If we run these functions, we get the following output:
The module works fine, but I want to make it more user-friendly. I want every function to first print a welcome message when it starts and then print a goodbye message before it finishes. This way, users have a better experience.
The simple but tedious way to do this is to modify each function to add the welcome and goodbye messages manually. However, that would mean repeating the same code for every function. Plus, if I ever add new functions in the future, I'd have to remember to add those messages again. This is a lot of repetitive work, which makes the code harder to maintain.
But here's the good news: Python decorators can solve this issue in a much better way. By using a decorator, I can add the exact behavior I need without touching the core logic of each function. Let’s take a look at how decorators make this possible (note the @greeting_decorator below):
Now, when running the same main.py as before, the output will look like this:
As you can see, adding @greeting_decorator before each function did the job! With just one line, we added a welcome and goodbye message to each function without changing the core function code.
In this article, you will learn how the @greeting_decorator works and how to create your own decorators. By the end, you'll understand how decorators can help you write cleaner and more readable code.
Understanding Function Objects
Let’s start by understanding functions a little better. Functions in Python are more than just blocks of code — they are actually first-class objects. This means you can use them like any other object. You can assign them to variables, pass them as arguments, and even return them from other functions.
Here’s an example of a simple function:
Functions Can Be Called
The most common way to use a function is by calling it with parentheses. For example:
When you add parentheses greet(), you are actually executing the function. If you use just greet without parentheses, you're referring to the function itself.
Functions Are Regular Python Objects
In Python, functions are objects, just like strings or numbers. You can assign a function to a variable, check its type, or even print it:
Note: When accessing a function as an object, don't use parentheses. You’re not calling the function — you're just referring to it.
Because functions are objects, you can also add attributes to them, which can be very useful in some cases. This makes functions in Python very flexible.
Functions Can Be Passed as Parameters
Since functions are objects, they can also be passed as arguments to other functions. This is really powerful, as it allows you to extend or change the behavior of a function without modifying it directly. Here’s an example:
In this code, we have a wrapper function that takes another function (other_function) as a parameter. It calls other_function (which is our greet function), which is why you see the output “Hello All!”. Passing functions as arguments like this is very useful for creating decorators because it allows us to wrap additional behavior around a function.
Functions Can Create and Return Other Functions
The final concept to understand before we make our first decorator is that functions can also be created inside other functions and returned. Here’s an example:
In the example above, display_quote is defined inside create_quote. The function create_quote doesn’t just run the inner function — it returns it, which allows us to call display_quote later.
Now, let’s look at how we can use the create_quote function:
The created_function variable is actually a function, and when we call it, it prints the quote:
This ability to create and return functions is what makes decorators so powerful. By wrapping one function around another, we can modify its behavior in a clean and reusable way.
OK, now you’re ready to create your first Python decorator! We will create the greeting_decorator that you saw earlier.
Implementing a Decorator
So, what exactly is a decorator? A decorator is a function that takes another function as an argument, defines an inner function that adds some behavior, and then returns that inner function. This might sound complicated, but it’s not too difficult when you see it in action.
Here’s what we want our decorator to do:
- Print a welcome message.
- Call the original function.
- Print a goodbye message.
We want this behavior to be added to our original functions without modifying them. Instead, we’ll use a decorator to wrap the original functions with this new behavior.
Here’s how we implement the greeting_decorator:
In greeting_decorator, we define an inner function called wrapper_func that prints a welcome message, calls the original function (func()), and then prints a goodbye message. Finally, we return wrapper_func.
To use the decorator, we just need to add @greeting_decorator before our function definition, like this:
This @greeting_decorator syntax is a shortcut for writing:
# Pseudo-code
factorial = greeting_decorator(factorial)
Instead of pointing factorial to its original code, it now points to the wrapper_func returned by greeting_decorator. So, when we call factorial(), we are actually calling the wrapper function that adds the extra behavior.
That’s why, after decorating our factorial function with @greeting_decorator, the function now prints messages before and after running the original factorial code.
Decorators are very helpful in Python. They let you change or extend the behavior of functions without modifying the original code. This is especially useful for tasks like logging, validation, or access control, which can be used across many functions without repeating code.
Conclusion
I hope this guide has given you a solid understanding of Python decorators. By using decorators, you can write cleaner, more reusable, and more maintainable code.
Feel free to try out the code examples yourself to see how decorators work. You can find the code on my GitHub and Google Colab.
Decorators are often seen in frameworks such as Flask and Django, where they are used for tasks like request handling, access control, and setting up middleware. Their flexibility allows you to write cleaner, more efficient code that’s easy to read and understand.