How to use decorators in Python

Have you ever wanted to add features to a function without altering its code? What if you could enhance functions in Python effortlessly without rewriting any existing logic? Imagine you’re preparing a dish, and instead of changing the recipe, you add a seasoning to enhance the flavor. In Python, decorators achieve something similar for your code, allowing you to modify the behavior of a function without changing its original content.

Let’s explore why decorators are important and how they work.

Key takeaways:

  • Understanding decorators: Decorators in Python modify the behavior of functions or methods without changing their actual code. They are useful for adding extra functionality like logging, access control, or caching.

  • How decorators work: A decorator is a function that takes another function as input and returns a modified version of it. The decorator “wraps” the original function, adding new behavior either before or after the function runs.

  • Core concepts: Python allows functions to be assigned to variables, defined within other functions, and passed as arguments. These concepts are fundamental to creating decorators.

  • Creating decorators: Decorators can be created by defining an outer function that returns a wrapper function. This wrapper function runs the original function and can add additional behavior.

  • Advanced decorators: You can create decorators that accept arguments, apply multiple decorators to a single function, and use the functools.wraps to preserve metadata when debugging. Decorators can also handle functions with different argument types using *args and **kwargs.

  • Use cases: Decorators are versatile and used in scenarios like timing function executions, repeating function calls, or creating custom behavior based on decorator parameters.

How to create decorators

A decorator is a function that changes the behavior of another function without changing its actual code. Decorators work by “wrapping” the function and adding new functionality or features around it.

For example, if we have a basic function and want to log a message each time it runs. We don’t need to change the function itself. We can use a decorator to enhance the function with this additional feature.

Before learning about decorators, we’ll first understand the properties of functions. Then, we’ll go through a simple step-by-step guide to creating decorators in different cases.

Properties of functions

1. Assigning functions to a variable

One of the basic concepts behind decorators is that functions can be treated like any other variable. We can assign a function to a variable and call it using that variable.

Example

Let’s look at the code below:

def greet(name):
return "Hello, {}!".format(name)
say_hello = greet # Assigning function to a variable
print(say_hello("Alice")) # Output: Hello, Alice!

Try replacing “Alice" with your name and see how the program behaves when you “Run” it.

2. Defining functions inside another function

In Python, we can define a function inside another function. This is often useful when creating a decorator, as the decorator function itself contains an inner function that wraps the original function.

Example

Let’s look at the code below:

def outer_function():
def inner_function():
print("This is an inner function")
inner_function()

3. Passing functions as arguments

Functions can also be passed as arguments to other functions. This is a key aspect of how decorators work, as a decorator takes a function as input and returns a modified version of that function.

Example

Let’s look at the code below:

def apply_function(func):
return func()
def say_hello():
return "Educative!"
print(apply_function(say_hello)) # Output: Hello!

4. Returning functions

Not only can functions be passed as arguments, but they can also return other functions. This forms the foundation of decorators, which typically return a wrapped version of the original function.

Example

Let’s look at the code below:

def outer_function():
def inner_function():
return "Hello Educative"
return inner_function
greeting = outer_function()
print(greeting()) # Output: Hello from inner!

5. Nested functions and scope variables

Functions defined inside other functions (nested functions) have access to variables in the enclosing function’s scope. This is known as a closure and is useful when defining decorators, as it allows the decorator to maintain state or context across function calls.

Example

Let’s look at the code below:

def outer_function(message):
def inner_function():
print(message)
return inner_function
func = outer_function("Hello, World!")
func() # Output: Hello, World!

How decorators work

A decorator is a special function in Python that modifies another function without changing its code. Let’s start with a basic example to understand how decorators work.

1. Simple decorator

  1. Define a function (decorator) that takes another function as its input.

  2. Create an inner function (wrapper) that adds the extra functionality.

  3. Return the wrapper function.

Example

Let’s look at the code below:

# Define the decorator
def simple_decorator(func):
def wrapper():
print("Function is about to run!")
func()
print("Function has finished running!")
return wrapper
# Apply the decorator
@simple_decorator
def greet():
print("Hello, World!")
# Call the decorated function
greet()

2. Multiple decorators

We can apply multiple decorators to a single function. Decorators are applied from top to bottom in the order they are defined.

Example

Let’s look at the code below:

def decorator_one(func):
def wrapper():
print("Decorator One")
func()
return wrapper
def decorator_two(func):
def wrapper():
print("Decorator Two")
func()
return wrapper
@decorator_one
@decorator_two
def show_message():
print("Hello!")
show_message()
# Output:
# Decorator One
# Decorator Two
# Hello!

3. Arguments accepted by decorators

We can also define decorators that accept arguments. This is useful when we want to create decorators with configurable behavior.

Example

Let’s look at the code below:

def decorator_with_args(times):
def decorator(func):
def wrapper():
for _ in range(times):
func()
return wrapper
return decorator
@decorator_with_args(3)
def say_hello():
print("Hello!")
say_hello()
# Output:
# Hello!
# Hello!
# Hello!

4. General-purpose decorators

We can write general-purpose decorators that work with functions of any number of arguments and keyword arguments by using *args and **kwargs.

Example

Let’s look at the code below:

def general_decorator(func):
def wrapper(*args, **kwargs):
print("Wrapper executed")
return func(*args, **kwargs)
return wrapper
@general_decorator
def greet(name, age):
print("Hello, my name is {} and I am {} years old".format(name, age))
greet("John", 30)

5. Arguments passing to the decorator

If we want the decorator to take arguments, we can add another level of nesting to the decorator function. This allows customization when the decorator is applied.

Example

Let’s look at the code below:

def repeat(num_times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(4)
def greet(name):
print("Educative".format(name))
greet("Alice")

6. Debug decorators

When debugging decorators, it’s important to preserve the original function’s metadata like its name and docstring. Python’s functools.wraps can be used to retain this information in the wrapper function.

Example

Let’s look at the code below:

import functools
def debug_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Calling {}".format(func.__name__)) # Use .format() instead of f-strings
return func(*args, **kwargs)
return wrapper
@debug_decorator
def say_hello():
print("Hello!")
say_hello()

Let’s take a quiz below:

1

What is the primary purpose of using a decorator in Python?

A)

To modify the underlying code of a function directly

B)

To change the function’s behavior without altering its code

C)

To execute the function only once

Question 1 of 20 attempted

Ready to deepen your Python knowledge? check out our “Become a Python Developer” path, beginning with Python’s basic concepts and advancing to more complex topics, preparing you for a career as a Python developer.

Frequently asked questions

Haven’t found what you were looking for? Contact Us


When should you use decorators in Python?

You should use decorators when you want to add reusable functionality to functions or methods without modifying their original code. They are perfect for tasks like logging, access control, memoization, or performance tracking, allowing you to wrap behavior around a function.


Why use class decorators in Python?

Class decorators are useful when you want to add behavior to or modify a class in a reusable manner, much like function decorators do for functions. They allow you to modify the class’s attributes or methods without directly altering the class’s definition.


What is the difference between class and decorator?

A class in Python is a blueprint for creating objects, encapsulating data and behavior, while a decorator is a higher-order function that wraps another function or class, adding extra functionality. Class decorators modify the behavior of a class, while regular decorators modify functions.


How do multiple decorators affect the execution flow of a function in Python?

Multiple decorators are applied in a stacked order, from top to bottom, and each decorator wraps the function, modifying its behavior before or after the function execution.


Free Resources