Python Decorators

Introduction

Python decorators allow you to add functionality to existing functions without modifying their original code directly. They are based on the decorator pattern of software development, where an object adds behavior to another object. They are especially useful for adding common functionality such as logging, authentication, performance monitoring, and caching to specific functions you implement. To crate a decorator, you don’t need to use new Python features beyond that of functions, however Python does have specialized @ syntax that can simplify the process.

Understanding Decorators

In Python, a decorator is a function that takes another function as an argument and returns a modified function based on it. (I’ll be using the term “decorator” and “decorator function” interchangeably.) This modified function typically includes additional behavior alongside the original function’s execution.

Here is sample code that demonstrates the basic structure of a decorator:

def decorator_function(original_function):
    def modified_function(*args, **kwargs):
        # Add functionality here
        original_function(*args, **kwargs)  # Use the original function
        # Add functionality here
        
    return modified_function

These are the key elements:

  • decorator_function: The decorator function that takes the original function as its argument.
  • original_function: The function to be modified, i.e. decorated.
  • modified_function: The modified version of the original function. This function is also referred to as the decorating or wrapper function.
  • *args, **kwargs: A generic parameter declaration that enables modified_function to enhance a function that has any combination of parameters.

In the code above, decorator_function() is the decorator. It accepts a function in the original_function parameter and defines an inner function modified_function(), which calls original_function() and runs some additional code. Inside modified_function(), you can have any combination of valid Python code and usage of original_function(). The last thing that decorator_function() does, is return modified_function. The decorator can now be used to create a decorated function, which it returns.

Note that the return statement of decorator_function (line 7) does not use modified_function with parentheses and an argument list because it doesn’t call that function. Instead, it returns the function itself, which can be called later.

Here is sample code that demonstrates how a decorator can be used:

def some_function():
    ...
    
decorated_function = decorator_function(some_function)

decorated_function()

The code above defines a sample function some_function() (line 1), and then decorates it with decorator_function() by passing some_function as an argument (line 4). The call to decorator_function() returns a decorated function, which is assigned to the variable decorated_function (line 4 again). Now, decorated_function contains a callable function, which can be used like a any other Python function (line 6).

Creating a Simple Decorator

The following is a simple example, which creates a decorator that prints some information about a function every time it’s called. The information includes the function’s name and return value.

Example:

def print_function_info_decorator(fn):
    def decorated(*args, **kwargs):
        returned_value = fn(*args, **kwargs)
        print(f"The function {fn.__name__}() was called and it returned {returned_value}.")
        return returned_value
        
    return decorated
    
def square(x): 
    return x**2
    
def hello():
    print("Hello")
    
traced_square = print_function_info_decorator(square)

traced_square(3)
traced_square(5)
traced_square(7)

traced_hello =  print_function_info_decorator(hello)

traced_hello()

Output:

The function square() was called and it returned 9.
The function square() was called and it returned 25.
The function square() was called and it returned 49.
Hello
The function hello() was called and it returned None.

In the example above, print_function_info_decorator() is the decorator, and it receives a function through its fn parameters (line 1). The inner function, decorated(), (line 2) modifies fn by printing its name and return value (line 4). Other than the information printout, decorated() does exactly the same thing as the original fn. This is accomplished by first calling the original function, assigning its returned value to the returned_value variable (line 3), and then returning returned_value (line 5) after the print() call (line 4). Following decorator’s definition, the example demonstrates how it can be used by defining and decorating the square() and hello() functions (lines 9-23).

The @ Syntax of Decorators

Python provides convenient syntax using the ‘at’ (@) symbol, which applies a decorator to function when it’s defined. This syntax enhances readability and simplifies the process of decorating functions.

Here is how you can use the @ syntax to apply a decorator:

@decorator_function
def my_function():
    ... # function code goes here

These are the key elements of the @ syntax:

  • @decorator_function: The @ symbol followed by the name of the decorator function. (The decorator function must be previously defined.)
  • def my_function():: The definition of the function to be decorated immediately follows the decorator.

It’s important to note that decorating a function using ‘@‘ replaces the original definition of the function with its decorated version.

@ Under the Hood

The @ syntax is effectively just shorthand to existing Python code you’ve seen at the beginning sections of this lesson.

This code,

@decorator_function
def my_function():
    ... # function code goes here

Is equivalent to the following code:

def my_function():
    ... # function code goes here

my_function = decorator_function(my_function)

The @ syntax can be understood as shorthand for the above. It assigns the original function (my_function), to the decorated version (decorator_function(my_function)), which replaces it (line 4).

@ Syntax Example

The example below reuses the same print_function_info_decorator() decorator in the prior example. But this time, it applies the @ syntax to decorate a function.

Example:

@print_function_info_decorator
def barking_dog():
    return "Woof! Woof!"

print(barking_dog())

Output:

The function dog_bark() was called and it returned Woof! Woof!.
Woof! Woof!

In the example above, the dog_bark() function is decorated by the print_function_info_decorator decorator using the @ syntax (line 1). This decorator adds functionality to the dog_bark() function by printing its information. When dog_bark() is called (line 5), it returns the string “Woof! Woof!”, which is printed to the console. Additionally, the decorator prints a message indicating the name of the function called and the value it returned. Since barking_dog() is decorated with @, the original barking_dog() definition is replaced by a function that incorporates the functionality of the decorator.

Using Pre-Made Decorators

While creating your own custom decorator can be useful, Python also provides made-for-you decorators that you can readily use and conveniently apply with the @ syntax. These pre-made decorators are available in various Python libraries and frameworks, offering functionalities such as caching, authentication, rate limiting, and more. Leveraging these pre-made decorators can save you time and effort in implementing common functionalities in your applications.

Here are some examples of popular libraries/frameworks with pre-made decorators:

  • Flask: Flask provides decorators for routing HTTP requests to view functions, handling request methods, managing session data, and more.
  • Django: Django offers decorators for URL routing, handling HTTP methods, managing authentication and permissions, caching views, and more.
  • functools: The functools module in Python’s standard library provides decorators such as lru_cache, which implements memoization (caching) for functions, improving performance by storing results of expensive function calls.
  • pytest: Pytest provides decorators for test functions, allowing you to mark tests with attributes like @pytest.mark.parametrize for parameterized testing, @pytest.fixture for defining fixtures, and more.
  • Flask-Restful: This Flask extension offers decorators for building RESTful APIs, simplifying the creation of resources, defining request parsers, handling authentication, and more.

Using pre-made decorators not only saves development time but also promotes code consistency and reliability, as these decorators are often maintained and tested by the community. When working on your projects, always explore available libraries and frameworks to see if there are decorators that meet your requirements before reinventing the wheel.

Summary & Reference for Python Decorators

Python decorators allow you to add functionality to existing functions without modifying their original code directly.


To define a decorator, you create a function that takes another function as an argument and returns a modified version of that function. This modified function typically includes additional behavior alongside the original function’s execution.

def decorator_function(original_function):
    def modified_function(*args, **kwargs):
        # Add functionality here
        original_function(*args, **kwargs)  
        # Add functionality here
    return modified_function

You can apply a decorator by calling the decorator function with the function to be decorated and assigning it to a variable.

def some_function():
    ...
    
decorated_function = decorator_function(some_function)
decorated_function()

You can also apply decorators to functions using the convenient @ syntax, which replaces the original function with the decorated one.

@decorator_function
def my_function():
    ... # function code goes here

Python also provides built-in decorators and libraries with pre-made decorators for common functionalities like caching, authentication, and URL routing in frameworks like Flask and Django.