Decorators in Python can be a confusing topic, particularly for newer students. That's why we've made a four-part video course for you to go through, linked below.
I've also put together some written explanations of the important concepts, so if you're more of a reader, or if you just want more examples, you'll find all of the information you need below.
Higher order functions
Before learning about decorators, you must know a little bit about higher order functions. After all, all decorators are higher order functions (but not all higher order functions are decorators!).
So what is are higher order functions?
They're just functions that take other functions as arguments. Higher order functions usually call the functions passed to them, but they generally involve additional operations as well.
For example, let's say you have a function called say_hello
:
def say_hello():
user_name = input("Enter your name: ")
print(f"Hello, {user_name}!")
Let's say that you want to create another function that runs this one, but also prints a welcome message before running.
You could do this:
def welcome_user():
print("Welcome to my application!")
say_hello()
This is not a higher order function. It's just a function!
If you wanted to be able to print the welcome message before any arbitrary function, you should create a higher order function that takes in another function as an argument. Like so:
def welcome_user(func):
print("Welcome to my application!")
func()
The benefit of this is that now any function can be passed to welcome_user
, so it is very reusable. This particular example may not seem terribly useful, but we'll look at a more interesting one later on in this post!
You'd use this like so:
def welcome_user(func):
print("Welcome to my application!")
func()
def say_hello():
user_name = input("Enter your name: ")
print(f"Hello, {user_name}!")
welcome_user(say_hello) # Calls both functions and prints both things.
What are decorators?
A decorator is a higher order function that doesn't directly call the argument; instead, it creates another function that does, and returns that.
That's a lot of "function" in one phrase, so let's break it down!
Here's what a decorator might look like:
def decorator():
def say_goodbye():
print("Goodbye!")
return say_goodbye
The decorator
function defines an inner function, say_goodbye
, and then returns it. The inner function doesn't run until it is called. You could use it like this:
my_variable = decorator()
my_variable() # Goodbye!
Although it seems that my_variable
is not a function (no def
keyword there!), my_variable
is very much a function.
Remember that the right side of an assignment is evaluated before the assignment operation takes place. As such, the return value of decorator
, the say_goodbye
function, is assigned to my_variable
.
So, is decorator
a decorator?
Not yet.
In order to be a decorator, it needs to take a function as an argument:
def decorator(func):
def say_goodbye():
func()
print("Goodbye!")
return say_goodbye
You could then use this decorator like so:
def say_hello():
print("Hello!")
greet = decorator(say_hello)
greet() # Hello! and Goodbye!
Now decorator
is worthy of its name. It takes a function as an argument and returns a new function that essentially "extends" it. Instead of just calling the original function, the new greet
function does more.
Usually you'd use decorators to replace existing functions and simultaneously extend them:
def say_hello():
print("Hello!")
say_hello = decorator(say_hello)
say_hello() # Hello! and Goodbye!
In that last example, the decorator
function runs first since it's the right-hand side of the assignment. The original say_hello
function will be called only within the say_goodbye
function defined inside the decorator.
After line 4, the say_hello
variable in the global scope no longer refers to the original function. Instead it refers to the say_goodbye
function.
You can check this by printing out the __name__
:
def say_hello():
print("Hello!")
say_hello = decorator(say_hello)
print(say_hello.__name__) # say_goodbye
This is a drawback of these incomplete decorators, in that the old function's __name__
and __doc__
properties are replaced by those of the new function. At the end of the day, we're completely replacing the variable with a new value, so this makes sense.
However often when using decorators we want to extend the say_hello
function but let it keep its name and documentation (if it has any). This is where the built-in module functools
comes in.
If you do this, the function will keep its original name and documentation:
import functools
def decorator(func):
@functools.wraps(func)
def say_goodbye():
func()
print("Goodbye!")
return say_goodbye
This now means the name is preserved:
def say_hello():
print("Hello!")
say_hello = decorator(say_hello)
print(say_hello.__name__) # say_hello
But, wait! What's that @
syntax?
Well, let's learn about it!
The @
syntax for decorators
The @
syntax is used to apply a decorator. When we wrote @functools.wraps(func)
earlier, we applied the wraps
decorator to our say_goodbye
function.
We can use that syntax with our code too. At the moment, our code looks like this:
import functools
def decorator(func):
@functools.wraps(func)
def say_goodbye():
func()
print("Goodbye!")
return say_goodbye
def say_hello():
print("Hello!")
say_hello = decorator(say_hello)
say_hello()
But using the @
syntax, it can be simplified to this:
import functools
def decorator(func):
@functools.wraps(func)
def say_goodbye():
func()
print("Goodbye!")
return say_goodbye
@decorator
def say_hello():
print("Hello!")
say_hello()
Note that we've gotten rid of the line that re-assigned say_hello
, and now instead we've placed @decorator
above the function definition.
This does exactly the same: it creates the function, and then passes it to the decorator, and re-assigns it.
If you run the code, you'll see that calling say_hello()
still prints out both "Hello!" and "Goodbye!".
A key benefit of this is that the function is immediately decorated, so it cannot be called accidentally before it has been decorated.
Decorating functions with parameters
At this point, our say_hello
function is being replaced with say_goodbye
. That means that if say_hello
had any parameters, those are lost when we replace the function. See this example:
import functools
def decorator(func):
@functools.wraps(func)
def say_goodbye():
func()
print("Goodbye!")
return say_goodbye
@decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Bob") # TypeError, say_goodbye() takes 0 positional arguments but 1 was given
Indeed if we were to do this, we would need to add the parameter to say_goodbye
as well:
def decorator(func):
@functools.wraps(func)
def say_goodbye(name):
func(name)
print("Goodbye!")
return say_goodbye
But this is a bad idea, because it couples our decorator to our say_hello
function, and that means we cannot use that decorator with other functions that have different numbers of parameters.
Instead the recommended approach is to generalise our decorators by making them take any number of arguments, and pass arguments (if any) to the decorated function as well.
We'll use the standard syntax *args, **kwargs
in order to do that. If you haven't encountered them before, here's a quick guide:
def decorator(func):
@functools.wraps(func)
def say_goodbye(*args, **kwargs):
func(*args, **kwargs)
print("Goodbye!")
return say_goodbye
Now this decorator can be used with any function, no matter how many parameters they have!
Writing decorators with parameters
In order to learn about decorators with parameters, let's take a look at another example. This example is also covered in the videos linked at the top of this post, so do check those out if you haven't already!
Let's say you have a function, get_admin_password
, that returns the password to the admin panel, and another function, get_user_password
, that returns the password to the user dashboard. They looks like this:
def get_admin_password():
return "1234"
def get_user_password():
return "secure_password"
You can call one of these functions like so:
print(get_admin_password()) # 1234
But naturally, you'd like only administrators to be able to get the admin password, and users to be able to get the user panel password.
This is what your user looks like:
user = {"name": "Bob Smith", "access_level": "admin"}
So, let's write a decorator that secures the get_password
function so it's not exposed to any user!
We'll start by only allowing administrators to run the function:
def make_secure(func):
def secure_func(*args, **kwargs):
if user["access_level"] == "admin":
return func(*args, **kwargs)
return secure_func
A simple decorator, but uses everything we've talked about until now!
It defines an inner function, which will run the passed function, func
, only if the user's access level is "admin" at the time.
Here's the complete code:
import functools
user = {"name": "Bob Smith", "access_level": "admin"}
def make_secure(func):
@functools.wraps(func)
def secure_func(*args, **kwargs):
if user["access_level"] == "admin":
return func(*args, **kwargs)
return secure_func
@make_secure
def get_admin_password():
return "1234"
@make_secure
def get_user_password():
return "secure_password"
print(get_password("admin")) # 1234
print(get_password("user")) # None
Why does it print None
when we try to get the user panel password? Because the decorator never runs return func(*args, **kwargs)
, so it just returns None
by default—as all Python functions do.
At the moment, the make_secure
decorator only allows users to run the function if their access level is "admin"
. It does not allow users to run either function.
Indeed, we need two slightly different decorators, one that checks for an "admin"
level of permissions, and one that checks for a "user"
level of permissions.
But there would be a lot of duplication if we just define two decorators, so instead what we'll do is make them more dynamic by adding a parameter:
@make_secure("admin")
def get_admin_password():
return "1234"
@make_secure("user")
def get_user_password():
return "secure_password"
If we are to do this, we need to make some changes to the decorator itself!
Something very important about this new bit of syntax is that make_secure("admin")
will run first, and then it will be applied as a decorator with the @
syntax.
So our decorator must change into this:
def make_secure(access_level):
def decorator(func):
@functools.wraps(func)
def secure_func(*args, **kwargs):
if user["access_level"] == "admin":
return func(*args, **kwargs)
return secure_func
return decorator
We have added one more level, because make_secure
is no longer a decorator per se. It is now a function that creates a decorator, and then that is applied to the functions with the 'at' syntax.
So when you run @make_secure("admin")
, that'll first of all execute the make_secure
function and create the decorator
function.
@decorator
is then applied, but it already knows about the access level you provided.
This is all quite confusing, and it's one of the most convoluted pieces of code you'll see in Python. Very seldom do we have three-level deep nesting of functions one inside of another!
I'd recommend checking the video playlist for another explanation, with slightly different examples. The more you see this kind of structure, the more sense it makes, and the easier it is to understand!
Wrapping Up
If you're interested in improving your Python skills, and learning about many more Python topics, we'd love to have you on our Complete Python Course. It's a fully comprehensive course, over 30 hours long, covering all major Python topics! We think it's an awesome course, but if it's not for you, we also have a 30 day money back guarantee.
You can also sign up to our mailing list below, as we send discount codes to our subscribers every month!
Hope to see you there!