Skip to main content

Python Decorators Tutorial

In this tutorial, learn how to implement decorators in Python.
Oct 2018  · 7 min read

If you would like to learn about functions, take DataCamp's Python Data Science Toolbox (Part 1) course.

A decorator is a design pattern in Python that allows a user to add new functionality to an existing object without modifying its structure. Decorators are usually called before the definition of a function you want to decorate. In this tutorial, we'll show the reader how they can use decorators in their Python functions.

Functions in Python are first class citizens. This means that they support operations such as being passed as an argument, returned from a function, modified, and assigned to a variable. This is a fundamental concept to understand before we delve into creating Python decorators.

Start Learning Python For Free

Python Data Science Toolbox (Part 1)

3 hr
356.9K learners
Learn the art of writing your own functions in Python, as well as key concepts like scoping and error handling.

Assigning Functions to Variables

To kick us off we create a function that will add one to a number whenever it is called. We'll then assign the function to a variable and use this variable to call the function.

def plus_one(number):
    return number + 1

add_one = plus_one

Run and edit the code from this tutorial online

Open Workspace

Defining Functions Inside other Functions

Next, we'll illustrate how you can define a function inside another function in Python. Stay with me, we'll soon find out how all this is relevant in creating and understanding decorators in Python.

def plus_one(number):
    def add_one(number):
        return number + 1

    result = add_one(number)
    return result

Passing Functions as Arguments to other Functions

Functions can also be passed as parameters to other functions. Let's illustrate that below.

def plus_one(number):
    return number + 1

def function_call(function):
    number_to_add = 5
    return function(number_to_add)


Functions Returning other Functions

A function can also generate another function. We'll show that below using an example.

def hello_function():
    def say_hi():
        return "Hi"
    return say_hi
hello = hello_function()

Nested Functions have access to the Enclosing Function's Variable Scope

Python allows a nested function to access the outer scope of the enclosing function. This is a critical concept in decorators -- this pattern is known as a Closure.

def print_message(message):
    "Enclosong Function"
    def message_sender():
        "Nested Function"


print_message("Some random message")
Some random message

Creating Decorators

With these prerequisites out of the way, let's go ahead and create a simple decorator that will convert a sentence to uppercase. We do this by defining a wrapper inside an enclosed function. As you can see it very similar to the function inside another function that we created earlier.

def uppercase_decorator(function):
    def wrapper():
        func = function()
        make_uppercase = func.upper()
        return make_uppercase

    return wrapper

Our decorator function takes a function as an argument, and we shall, therefore, define a function and pass it to our decorator. We learned earlier that we could assign a function to a variable. We'll use that trick to call our decorator function.

def say_hi():
    return 'hello there'

decorate = uppercase_decorator(say_hi)

However, Python provides a much easier way for us to apply decorators. We simply use the @ symbol before the function we'd like to decorate. Let's show that in practice below.

def say_hi():
    return 'hello there'


Applying Multiple Decorators to a Single Function

We can use multiple decorators to a single function. However, the decorators will be applied in the order that we've called them. Below we'll define another decorator that splits the sentence into a list. We'll then apply the uppercase_decorator and split_string decorator to a single function.

def split_string(function):
    def wrapper():
        func = function()
        splitted_string = func.split()
        return splitted_string

    return wrapper
def say_hi():
    return 'hello there'

From the above output, we notice that the application of decorators is from the bottom up. Had we interchanged the order, we'd have seen an error since lists don't have an upper attribute. The sentence has first been converted to uppercase and then split into a list.

Accepting Arguments in Decorator Functions

Sometimes we might need to define a decorator that accepts arguments. We achieve this by passing the arguments to the wrapper function. The arguments will then be passed to the function that is being decorated at call time.

def decorator_with_arguments(function):
    def wrapper_accepting_arguments(arg1, arg2):
        print("My arguments are: {0}, {1}".format(arg1,arg2))
        function(arg1, arg2)
    return wrapper_accepting_arguments

def cities(city_one, city_two):
    print("Cities I love are {0} and {1}".format(city_one, city_two))

cities("Nairobi", "Accra")
My arguments are: Nairobi, Accra
Cities I love are Nairobi and Accra

Defining General Purpose Decorators

To define a general purpose decorator that can be applied to any function we use args and **kwargs. args and **kwargs collect all positional and keyword arguments and stores them in the args and kwargs variables. args and kwargs allow us to pass as many arguments as we would like during function calls.

def a_decorator_passing_arbitrary_arguments(function_to_decorate):
    def a_wrapper_accepting_arbitrary_arguments(*args,**kwargs):
        print('The positional arguments are', args)
        print('The keyword arguments are', kwargs)
    return a_wrapper_accepting_arbitrary_arguments

def function_with_no_argument():
    print("No arguments here.")

The positional arguments are ()
The keyword arguments are {}
No arguments here.

Let's see how we'd use the decorator using positional arguments.

def function_with_arguments(a, b, c):
    print(a, b, c)

The positional arguments are (1, 2, 3)
The keyword arguments are {}
1 2 3

Keyword arguments are passed using keywords. An illustration of this is shown below.

def function_with_keyword_arguments():
    print("This has shown keyword arguments")

function_with_keyword_arguments(first_name="Derrick", last_name="Mwiti")
The positional arguments are ()
The keyword arguments are {'first_name': 'Derrick', 'last_name': 'Mwiti'}
This has shown keyword arguments

Passing Arguments to the Decorator

Now let's see how we'd pass arguments to the decorator itself. In order to achieve this, we define a decorator maker that accepts arguments then define a decorator inside it. We then define a wrapper function inside the decorator as we did earlier.

def decorator_maker_with_arguments(decorator_arg1, decorator_arg2, decorator_arg3):
    def decorator(func):
        def wrapper(function_arg1, function_arg2, function_arg3) :
            "This is the wrapper function"
            print("The wrapper can access all the variables\n"
                  "\t- from the decorator maker: {0} {1} {2}\n"
                  "\t- from the function call: {3} {4} {5}\n"
                  "and pass them to the decorated function"
                  .format(decorator_arg1, decorator_arg2,decorator_arg3,
                          function_arg1, function_arg2,function_arg3))
            return func(function_arg1, function_arg2,function_arg3)

        return wrapper

    return decorator

pandas = "Pandas"
@decorator_maker_with_arguments(pandas, "Numpy","Scikit-learn")
def decorated_function_with_arguments(function_arg1, function_arg2,function_arg3):
    print("This is the decorated function and it only knows about its arguments: {0}"
           " {1}" " {2}".format(function_arg1, function_arg2,function_arg3))

decorated_function_with_arguments(pandas, "Science", "Tools")
The wrapper can access all the variables
    - from the decorator maker: Pandas Numpy Scikit-learn
    - from the function call: Pandas Science Tools
and pass them to the decorated function
This is the decorated function, and it only knows about its arguments: Pandas Science Tools

Debugging Decorators

As we have noticed, decorators wrap functions. The original function name, its docstring, and parameter list are all hidden by the wrapper closure: For example, when we try to access the decorated_function_with_arguments metadata, we'll see the wrapper closure's metadata. This presents a challenge when debugging.

'This is the wrapper function'

In order to solve this challenge Python provides a functools.wraps decorator. This decorator copies the lost metadata from the undecorated function to the decorated closure. Let's show how we'd do that.

import functools

def uppercase_decorator(func):
    def wrapper():
        return func().upper()
    return wrapper
def say_hi():
    "This will say hi"
    return 'hello there'


When we check the say_hi metadata, we notice that it is now referring to the function's metadata and not the wrapper's metadata.

'This will say hi'

It is advisable and good practice to always use functools.wraps when defining decorators. It will save you a lot of headache in debugging.

Python Decorators Summary

Decorators dynamically alter the functionality of a function, method, or class without having to directly use subclasses or change the source code of the function being decorated. Using decorators in Python also ensures that your code is DRY(Don't Repeat Yourself). Decorators have several use cases such as:

  • Authorization in Python frameworks such as Flask and Django
  • Logging
  • Measuring execution time
  • Synchronization

To learn more about Python decorators check out Python's Decorator Library.

Learn more about Python

Writing Functions in Python

4 hr
Learn to use best practices to write maintainable, reusable, complex functions with good documentation.
See DetailsRight Arrow
Start Course
See MoreRight Arrow

Reshaping Data with pandas in Python

Pandas DataFrames are commonly used in Python for data analysis, with observations containing values or variables related to a single object and variables representing attributes across all observations.
Richie Cotton's photo

Richie Cotton

Reshaping Data with tidyr in R

In this cheat sheet, you will learn how to reshape data with tidyr. From separating and combining columns, to dealing with missing data, you'll get the download on how to manipulate data in R.
Richie Cotton's photo

Richie Cotton

6 min

Data Quality Dimensions Cheat Sheet

In this cheat sheet, you'll learn about data quality dimensions, allowing you to ensure that your data is fit for purpose.
Joe Franklin's photo

Joe Franklin

3 min

An Introduction to Python Subprocess: Basics and Examples

Explore our step-by-step guide to running external commands using Python's subprocess module, complete with examples.
Moez Ali's photo

Moez Ali

15 min

Setting Up VSCode For Python: A Complete Guide

Experience a simple, fun, and productive way of Python development by learning about VSCode and its extensionsn and features.
Abid Ali Awan's photo

Abid Ali Awan

16 min

GeoPandas Tutorial: An Introduction to Geospatial Analysis

Get started with GeoPandas, one of the most popular Python libraries for geospatial analysis.
Javier Canales Luna's photo

Javier Canales Luna

15 min

See MoreSee More