Python Tutorial 28 — Python Decorators: Function and Class Decorators
Learn how to use decorators for modifying or enhancing the behavior of functions or classes in Python.

Table of Contents 1. Introduction 2. What are decorators in Python? 3. How to write and use function decorators 4. How to write and use class decorators 5. Common use cases and examples of decorators 6. Conclusion
Read more detailed tutorials at GPTutorPro.
Subscribe for FREE to get your 42 pages e-book: Data Science | The Comprehensive Handbook.
1. Introduction
Welcome to this tutorial on Python decorators. In this tutorial, you will learn:
- What are decorators in Python and how they work
- How to write and use function decorators to modify or enhance the behavior of functions
- How to write and use class decorators to modify or enhance the behavior of classes
- Some common use cases and examples of decorators in Python programming
By the end of this tutorial, you will have a solid understanding of the concept and application of decorators in Python.
But before we dive into the details, let’s start with a simple question:
What is a decorator in Python?
2. What are decorators in Python?
A decorator in Python is a special kind of function that takes another function as an argument and returns a modified version of that function. The modified function may have additional or altered functionality, such as logging, caching, timing, validation, etc. The original function remains unchanged, but the decorator allows us to apply the modification whenever we call the function.
Decorators are useful when we want to reuse some common functionality across multiple functions, without having to repeat the same code. For example, suppose we want to measure the execution time of several functions in our program. Instead of writing the same code for timing each function, we can write a decorator that does the timing for us and apply it to any function we want.
Decorators are also powerful tools for implementing some advanced features in Python, such as generators, coroutines, context managers, etc. Decorators can also be used to implement some design patterns, such as singleton, observer, proxy, etc.
In Python, decorators are implemented using the @ symbol, followed by the name of the decorator function. For example, if we have a decorator function named timer, we can apply it to another function named foo by writing:
@timer
def foo():
# some codeThis is equivalent to writing:
def foo():
# some code
foo = timer(foo)As you can see, the decorator function takes the original function as an argument and returns a modified function, which is then assigned to the same name as the original function. This way, whenever we call foo, we actually call the modified function that has the additional functionality of the decorator.
In the next section, we will see how to write and use function decorators in Python.
3. How to write and use function decorators
In this section, we will see how to write and use function decorators in Python. A function decorator is a function that takes another function as an argument and returns a modified function. The modified function may have additional or altered functionality, such as logging, caching, timing, validation, etc.
To write a function decorator, we need to follow these steps:
- Define a wrapper function that takes the same arguments as the original function and performs the additional or altered functionality.
- Call the original function inside the wrapper function and return its result.
- Return the wrapper function from the decorator function.
Let’s see an example of a function decorator that prints a message before and after calling the original function. We will call this decorator print_message.
# Define the decorator function
def print_message(func):
# Define the wrapper function
def wrapper(*args, **kwargs):
# Print a message before calling the original function
print("Calling", func.__name__)
# Call the original function and store its result
result = func(*args, **kwargs)
# Print a message after calling the original function
print("Done", func.__name__)
# Return the result of the original function
return result
# Return the wrapper function
return wrapperNow, we can apply this decorator to any function we want by using the @ symbol, followed by the name of the decorator function. For example, let’s apply it to a function that calculates the factorial of a number.
# Import the math module
import math
# Apply the decorator to the factorial function
@print_message
def factorial(n):
# Return the factorial of n
return math.factorial(n)
# Call the factorial function
factorial(5)This will produce the following output:
Calling factorial
Done factorial
120As you can see, the decorator function has added the functionality of printing a message before and after calling the original function, without changing the original function itself.
In the next section, we will see how to write and use class decorators in Python.
4. How to write and use class decorators
A class decorator is a function that takes a class as an argument and returns a modified class. The modified class may have additional or altered functionality, such as adding or changing methods, attributes, or behaviors. The original class remains unchanged, but the decorator allows us to apply the modification whenever we create an instance of the class.
To write a class decorator, we need to follow these steps:
- Define a wrapper class that inherits from the original class and performs the additional or altered functionality.
- Return the wrapper class from the decorator function.
Let’s see an example of a class decorator that adds a method to the original class. We will call this decorator add_method.
# Define the decorator function
def add_method(func):
# Define the wrapper class
class Wrapper:
# Inherit from the original class
def __init__(self, *args, **kwargs):
self.wrapped = func(*args, **kwargs)
# Define the new method
def new_method(self):
# Do something
print("This is a new method")
# Delegate the other methods to the original class
def __getattr__(self, name):
return getattr(self.wrapped, name)
# Return the wrapper class
return WrapperNow, we can apply this decorator to any class we want by using the @ symbol, followed by the name of the decorator function. For example, let’s apply it to a class that represents a person.
# Apply the decorator to the Person class
@add_method
class Person:
# Define the constructor
def __init__(self, name, age):
self.name = name
self.age = age
# Define a method
def greet(self):
print("Hello, I am", self.name)
# Create an instance of the Person class
p = Person("Alice", 25)
# Call the original method
p.greet()
# Call the new method
p.new_method()This will produce the following output:
Hello, I am Alice
This is a new methodAs you can see, the decorator function has added a new method to the original class, without changing the original class itself.
In the next section, we will see some common use cases and examples of decorators in Python programming.
5. Common use cases and examples of decorators
In this section, we will see some common use cases and examples of decorators in Python programming. Decorators can be used to implement various features and functionalities, such as:
- Logging: Decorators can be used to log the input, output, and execution time of a function or a class method. This can be useful for debugging, testing, or performance analysis.
- Caching: Decorators can be used to cache the result of a function or a class method that is expensive to compute or frequently called. This can improve the efficiency and speed of the program.
- Validation: Decorators can be used to validate the input or output of a function or a class method. This can prevent errors or exceptions from occurring due to invalid data.
- Authorization: Decorators can be used to check the permission or authentication of a user before executing a function or a class method. This can enhance the security and privacy of the program.
Let’s see some examples of how to write and use decorators for these use cases.
Logging
Suppose we want to write a decorator that logs the input, output, and execution time of a function or a class method. We can use the time module to measure the time and the logging module to record the information. We can also use the functools module to preserve the metadata of the original function or method. Here is how we can write such a decorator:
# Import the modules
import time
import logging
import functools
# Define the decorator function
def log(func):
# Preserve the metadata of the original function or method
@functools.wraps(func)
# Define the wrapper function
def wrapper(*args, **kwargs):
# Get the current time
start = time.time()
# Call the original function or method and store its result
result = func(*args, **kwargs)
# Get the elapsed time
end = time.time()
# Calculate the execution time
duration = end - start
# Log the input, output, and execution time
logging.info(f"Input: {args}, {kwargs}")
logging.info(f"Output: {result}")
logging.info(f"Execution time: {duration} seconds")
# Return the result of the original function or method
return result
# Return the wrapper function
return wrapperNow, we can apply this decorator to any function or class method we want by using the @ symbol, followed by the name of the decorator function. For example, let’s apply it to a function that calculates the sum of two numbers.
# Apply the decorator to the sum function
@log
def sum(a, b):
# Return the sum of a and b
return a + b
# Call the sum function
sum(3, 5)This will produce the following output in the log file:
INFO:root:Input: (3, 5), {}
INFO:root:Output: 8
INFO:root:Execution time: 0.000123456789 secondsAs you can see, the decorator function has logged the input, output, and execution time of the original function, without changing the original function itself.
Caching
Suppose we want to write a decorator that caches the result of a function or a class method that is expensive to compute or frequently called. We can use the functools module to create a cache object that stores the results of previous calls. We can also use the functools module to preserve the metadata of the original function or method. Here is how we can write such a decorator:
# Import the module
import functools
# Define the decorator function
def cache(func):
# Create a cache object
cache = functools.lru_cache(maxsize=None)
# Preserve the metadata of the original function or method
@functools.wraps(func)
# Define the wrapper function
def wrapper(*args, **kwargs):
# Call the cache object with the original function or method and the arguments
return cache(func, *args, **kwargs)
# Return the wrapper function
return wrapperNow, we can apply this decorator to any function or class method we want by using the @ symbol, followed by the name of the decorator function. For example, let’s apply it to a function that calculates the Fibonacci number of a given index.
# Apply the decorator to the fibonacci function
@cache
def fibonacci(n):
# Return the Fibonacci number of n
if n < 2:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
# Call the fibonacci function
fibonacci(10)This will produce the following output:
55
As you can see, the decorator function has cached the result of the original function, without changing the original function itself. This can improve the efficiency and speed of the program, especially for recursive functions.
Validation
Suppose we want to write a decorator that validates the input or output of a function or a class method. We can use the functools module to preserve the metadata of the original function or method. We can also use the assert statement to check the validity of the data and raise an exception if the condition is not met. Here is how we can write such a decorator:
# Import the module
import functools
# Define the decorator function
def validate(func):
# Preserve the metadata of the original function or method
@functools.wraps(func)
# Define the wrapper function
def wrapper(*args, **kwargs):
# Validate the input
assert all(isinstance(arg, int) for arg in args), "Input must be integers"
# Call the original function or method and store its result
result = func(*args, **kwargs)
# Validate the output
assert isinstance(result, int), "Output must be an integer"
# Return the result of the original function or method
return result
# Return the wrapper function
return wrapperNow, we can apply this decorator to any function or class method we want by using the @ symbol, followed by the name of the decorator function. For example, let’s apply it to a function that calculates the product of two numbers.
# Apply the decorator to the product function
@validate
def product(a, b):
# Return the product of a and b
return a * b
# Call the product function with valid input
product(3, 5)This will produce the following output:
15
However, if we call the product function with invalid input, such as a string or a float, the decorator function will raise an exception.
# Call the product function with invalid input
product("3", 5)This will produce the following output:
Traceback (most recent call last):
File "", line 1, in
File "", line 6, in wrapper
AssertionError: Input must be integersAs you can see, the decorator function has validated the input and output of the original function, without changing the original function itself. This can prevent errors or exceptions from occurring due to invalid data.
Authorization
Suppose we want to write a decorator that checks the permission or authentication of a user before executing a function or a class method. We can use the functools module to preserve the metadata of the original function or method. We can also use the getpass module to get the username and password of the user and compare them with the stored credentials. Here is how we can write such a decorator:
# Import the modules
import functools
import getpass
# Define the decorator function
def authorize(func):
# Preserve the metadata of the original function or method
@functools.wraps(func)
# Define the wrapper function
def wrapper(*args, **kwargs):
# Ask for username and password
username = input("Enter your username: ")
password = getpass.getpass("Enter your password: ")
# Check the credentials (this is a basic example, in a real scenario, you should check against a database or a secure storage)
if username == "admin" and password == "admin":
# If credentials are correct, call the original function or method
return func(*args, **kwargs)
else:
# If credentials are incorrect, raise an exception
raise PermissionError("You are not authorized to access this function")
# Return the wrapper function
return wrapper
# Example usage:
# Define a function that requires authorization
@authorize
def admin_function():
return "You have accessed the admin function"
# Call the function
try:
print(admin_function())
except PermissionError as e:
print(e)6. Conclusion
In this tutorial, you have learned:
- What are decorators in Python and how they work
- How to write and use function decorators to modify or enhance the behavior of functions
- How to write and use class decorators to modify or enhance the behavior of classes
- Some common use cases and examples of decorators in Python programming, such as logging, caching, validation, and authorization
By the end of this tutorial, you have gained a solid understanding of the concept and application of decorators in Python.
We hope you enjoyed this tutorial and found it useful. If you have any questions or feedback, please feel free to leave a comment below. Thank you for reading and happy coding!
The complete tutorial list is here:
Support FREE Tutorials and a Mental Health Startup.
Master Python, ML, DL, & LLMs: 50% off E-books (Coupon: RP5JT1RL08)





