Geekflare is supported by our audience. We may earn affiliate commissions from buying links on this site.
Share on:

Python Decorators: Explained (With Examples and Use Cases)

Python Decorators Explained (With Examples and Use Cases)
Invicti Web Application Security Scanner – the only solution that delivers automatic verification of vulnerabilities with Proof-Based Scanning™.

Python decorators are an incredibly useful construct in Python. Using decorators in Python, we can modify the behaviour of a function by wrapping it inside another function. Decorators enable us to write cleaner code and share functionality. This article is a tutorial on not only how to use decorators but how to create them.

Prerequisite knowledge

The topic of decorators in Python requires some background knowledge. Below, I have listed some concepts you should already be familiar with to make sense of this tutorial. I have also linked resources where you can brush up on the concepts if need be.

Basic Python

This topic is a more intermediate/advanced topic. As a result, before attempting to learn, you should already be familiar with the basics of Python, such as data types, functions, objects, and classes.

You should also understand some object-oriented concepts such as getters, setters, and constructors. If you are not familiar with the Python programming language, here are some resources to get you started.

Functions are first-class citizens

In addition to basic Python, you should also be aware of this more advanced concept in Python. Functions, and pretty much everything else in Python, are objects like int or string. Because they are objects, you can do a few things with them, namely:

  • You can pass a function as an argument to another function in the same way you pass a string or int as a function argument.
  • Functions can also be returned by other functions like you would return other string or int values.
  • Functions can be stored in variables

In fact, the only difference between functional objects and other objects is functional objects contain the magic method __call__().

Hopefully, at this point, you are comfortable with the prerequisite knowledge. We can begin discussing the main topic.

What is a Python Decorator?

A Python decorator is simply a function that takes in a function as an argument and returns a modified version of the function that was passed in. In other words, the function foo is a decorator if it takes in, as an argument, the function bar and returns another function baz.

The function baz is a modification of bar in the sense that within the body of baz, there is a call to the function bar. However, before and after the call to bar, baz can do anything. That was a mouthful; here is some code to illustrate the situation:

# Foo is a decorator, it takes in another function, bar as an argument
def foo(bar):

    # Here we create baz, a modified version of bar
    # baz will call bar but can do anything before and after the function call
    def baz():

        # Before calling bar, we print something
        print("Something")

        # Then we run bar by making a function call
        bar()

        # Then we print something else after running bar
        print("Something else")

    # Lastly, foo returns baz, a modified version of bar
    return baz

How to Create a Decorator in Python?

To illustrate how decorators are created and used in Python, I am going to illustrate this with a simple example. In this example, we will create a logger decorator function that will log the name of the function it is decorating every time that function runs.

To get started, we created the decorator function. The decorator takes in func as an argument. func is the function we are decorating.

def create_logger(func):
    # The function body goes here

Inside the decorator function, we are going to create our modified function that will log the name of func before running func.

# Inside create_logger
def modified_func():
    print("Calling: ", func.__name__)
    func()

Next, the create_logger function will return the modified function. As a result, our entire create_logger function will look like this:

def create_logger(func):
    def modified_func():
        print("Calling: ", func.__name__)
        func()

    return modified_function

We are done creating the decorator. The create_logger function is a simple example of a decorator function. It takes in func, which is the function we are decorating, and returns another function, modified_func. modified_func first logs the name of func, before running func.

How to use decorators in Python

To use our decorator, we use the @ syntax like so:

@create_logger
def say_hello():
    print("Hello, World!")

Now we can call say_hello() in our script, and the output should be the following text:

Calling:  say_hello
"Hello, World"
Screenshot of a program that uses python decorators

But what is the @create_logger doing? Well, it is applying the decorator to our say_hello function. To better understand what is doing, the code immediately below this paragraph would achieve the same result as putting @create_logger before say_hello.

def say_hello():
    print("Hello, World!")

say_hello = create_logger(say_hello)

In other words, one way to use decorators in Python is to explicitly call the decorator passing in the function as we did in the code above. The other and more concise way is to use the @ syntax.

In this section, we covered how to create Python decorators.

Slightly more complicated examples

The above example was a simple case. There are slightly more complex examples such as when the function we are decorating takes in arguments. Another more complicated situation is when you want to decorate an entire class. I am going to cover both of these situations here.

When the function takes in arguments

When the function you are decorating takes in arguments, the modified function should receive the arguments and pass them when it eventually makes the call to the unmodified function. If that sounds confusing, let me explain in foo-bar terms.

Recall that foo is the decorator function, bar is the function we are decorating and baz is the decorated bar. In that case, bar will take in the arguments and pass them to baz during the call to baz. Here is a code example to solidify the concept:

def foo(bar):
    def baz(*args, **kwargs):
        # You can do something here
        ___
        # Then we make the call to bar, passing in args and kwargs
        bar(*args, **kwargs)
        # You can also do something here
        ___

    return baz

If the *args and **kwargs look unfamiliar; they are simply pointers to the positional and keyword arguments, respectively.

It is important to note that baz has access to the arguments and can therefore perform some validation of the arguments before calling bar.

An example would be if we had a decorator function, ensure_string that would ensure that the argument passed to a function it is decorating is a string; we would implement it like so:

def ensure_string(func):
    def decorated_func(text):
        if type(text) is not str:
             raise TypeError('argument to ' + func.__name__ + ' must be a string.')
        else:
             func(text)

    return decorated_func

We could decorate the say_hello function like so:

@ensure_string
def say_hello(name):
    print('Hello', name)

Then we could test the code using this:

say_hello('John') # Should run just fine
say_hello(3) # Should throw an exception

And it should produce the following output:

Hello John
Traceback (most recent call last):
   File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in <module> say hello(3) # should throw an exception
   File "/home/anesu/Documents/python-tu$ ./decorators.pytorial/./decorators.py", line 7, in decorated_func raise TypeError('argument to + func._name_ + must be a string.')
TypeError: argument to say hello must be a string. $0
Screenshot-from-2023-05-23-02-33-18

As expected, the script managed to print ‘Hello John’ because ‘John’ is a string. It threw an exception when trying to print ‘Hello 3’ because ‘3’ wasn’t a string. The ensure_string decorator could be used to validate the arguments of any function that requires a string.

Decorating a class

In addition to just decorating functions, we can also decorate classes. When you add a decorator to a class, the decorated method replaces the class’s constructor/initiator method(__init__).

Going back to foo-bar, suppose foo is our decorator and Bar is the class we are decorating, then foo will decorate Bar.__init__. This will be useful if we want to do anything before objects of the type Bar are instantiated.

This means that the following code

def foo(func):
    def new_func(*args, **kwargs):
        print('Doing some stuff before instantiation')
        func(*args, **kwargs)

    return new_func

@foo
class Bar:
    def __init__(self):
        print("In initiator")

Is equivalent to

def foo(func):
    def new_func(*args, **kwargs):
        print('Doing some stuff before instantiation')
        func(*args, **kwargs)

    return new_func

class Bar:
    def __init__(self):
        print("In initiator")


Bar.__init__ = foo(Bar.__init__)

In fact, instantiating an object of class Bar, defined using either of the two methods, should give you the same output:

Doing some stuff before instantiation
In initiator
Screenshot-from-2023-05-23-02-20-38

Example Decorators in Python

While you can define your own decorators, there are some that are already built into Python. Here are some of the common decorators you may encounter in Python:

@staticmethod

The static method is used on a class to indicate that the method it is decorating is a static method. Static methods are methods that can run without the need to instantiate the class. In the following code example, we create a class Dog with a static method bark.

class Dog:
    @staticmethod
    def bark():
        print('Woof, woof!')

Now the bark method can be accessed like so:

Dog.bark()

And running the code would produce the following output:

Woof, woof!
Screenshot-from-2023-05-23-02-02-07

As I mentioned in the section on How to use Decorators, decorators can be used in two ways. The @ syntax being the more concise being one of the two. The other method is to call the decorator function, passing in the function we want to decorate as an argument. Meaning the code above achieves the same thing as the code below:

class Dog:
    def bark():
        print('Woof, woof!')

Dog.bark = staticmethod(Dog.bark)

And we can still use the bark method in the same way

Dog.bark()

And it would produce the same output

Woof, woof!
Screenshot-from-2023-05-23-02-02-07-1

As you can see, the first method is cleaner and it’s more obvious that the function is a static function before you have even started reading the code. As a result, for the remaining examples, I will use the first method. But just remember the second method is an alternative.

@classmethod

This decorator is used to indicate that the method it is decorating is a class method. Class methods are similar to static methods in that they both do not require that the class be instantiated before they can be called.

However, the main difference is that class methods have access to class attributes while static methods do not. This is because Python automatically passes the class as the first argument to a class method whenever it is called. To create a class method in Python, we can use the classmethod decorator.

class Dog:
    @classmethod
    def what_are_you(cls):
        print("I am a " + cls.__name__ + "!")

To run the code, we simply call the method without instantiating the class:

Dog.what_are_you()

And the output is:

I am a Dog!
Screenshot-from-2023-05-23-02-07-18

@property

The property decorator is used to label a method as a property setter. Going back to our Dog example, let’s create a method that retrieves the name of the Dog.

class Dog:
    # Creating a constructor method that takes in the dog's name
    def __init__(self, name):

         # Creating a private property name
         # The double underscores make the attribute private
         self.__name = name

    
    @property
    def name(self):
        return self.__name

Now we can access the dog’s name like a normal property,

# Creating an instance of the class
foo = Dog('foo')

# Accessing the name property
print("The dog's name is:", foo.name)

And the result of running the code would be

The dog's name is: foo
Screenshot-from-2023-05-23-02-09-42

@property.setter

The property.setter decorator is used to create a setter method for our properties. To use the @property.setter decorator, you replace property with the name of the property, you are creating a setter for. For example, if you are creating a setter for the method for the property foo, your decorator will be @foo.setter. Here is a Dog example to illustrate:

class Dog:
    # Creating a constructor method that takes in the dog's name
    def __init__(self, name):

         # Creating a private property name
         # The double underscores make the attribute private
         self.__name = name

    
    @property
    def name(self):
        return self.__name

    # Creating a setter for our name property
    @name.setter
    def name(self, new_name):
        self.__name = new_name

To test the setter, we can use the following code:

# Creating a new dog
foo = Dog('foo')

# Changing the dog's name
foo.name = 'bar'

# Printing the dog's name to the screen
print("The dog's new name is:", foo.name)

Running the code will produce the following output:

The dogs's new name is: bar
Screenshot-from-2023-05-23-11-46-25

Importance of decorators in Python

Now that we have covered what decorators are, and you have seen some examples of decorators, we can discuss why decorators are important in Python. Decorators are important for several reasons. Some of them, I have listed below:

  • They enable code reusability: In the logging example given above, we could use the @create_logger on any function we want. This allows us to add logging functionality to all our functions without manually writing it for each function.
  • They allow you to write modular code: Again, going back to the logging example, with decorators, you can separate the core function, in this case say_hello from the other functionality you need, in this case, logging.
  • They enhance frameworks and libraries: Decorators are extensively used in Python frameworks and libraries to provide additional functionality. For example, in web frameworks like Flask or Django, decorators are used for defining routes, handling authentication, or applying middleware to specific views.

Final Words

Decorators are incredibly useful; you can use them to extend functions without altering their functionality. This is useful when you want to time the performance of functions, log whenever a function is called, validate arguments before calling a function, or verify permissions before a function is run. Once you understand decorators, you will be able to write code in a cleaner way.

Next, you may want to read our articles on tuples and using cURL in Python.

Thanks to our Sponsors
More great readings on Development
Power Your Business
Some of the tools and services to help your business grow.
  • Invicti uses the Proof-Based Scanning™ to automatically verify the identified vulnerabilities and generate actionable results within just hours.
    Try Invicti
  • Web scraping, residential proxy, proxy manager, web unlocker, search engine crawler, and all you need to collect web data.
    Try Brightdata
  • Monday.com is an all-in-one work OS to help you manage projects, tasks, work, sales, CRM, operations, workflows, and more.
    Try Monday
  • Intruder is an online vulnerability scanner that finds cyber security weaknesses in your infrastructure, to avoid costly data breaches.
    Try Intruder