Python: Decorator

The decorator helps us add some processing steps before and/or after executing an original function. These common tasks can be anything, like- checking access, object cleanup, setup caching or logging, etc. but that depends on your implementation.

Using a decorator, we can add some additional steps before and/or after the original function execution, without touching/changing the original function.

In Python –

A decorator is a function that accepts a function and returns a function.

We can perform the additional steps of operation(that we want to add before/after the original function), in the implementation of the decorator.

NOTES

In Python, functions are first-class entities. That is why we can-

Pass them as arguments to other functions.
Return then from other functions.

Decorator Capability/Rules

Here is what a Python Decorator can and can not do-

Decorators can do

Change the behavior of the function.
Add steps before and/or after the function execution.
Chain multiple decorators for the same function.
Access function metadata, using tools like functools.
Change the way the parameters are passed to the function.
Can be applied to a normal function, class method, or entire class.
Can maintain state within the decorator(for class-based decorator)
Control the execution flow of the function, by passing appropriate param or based on some state.

Decorators can not do

Can not directly change the original function.
Can not alter the original function signature.
Can not change function context dynamically.
Can not bypass Python scope rules.
Can not directly work on non-callable objects.

Basic Understanding

WARNING

This is not the exact implementation of the Decorator, but the initial step to understand the Decorator implementation.

Make sure to check this before going to the next step.

First, let’s see what is going on in the decorator implementation. As a decorator is a function that accepts a function and returns a function to create such a function first.

Decorator Function:

Create a function named “my_decorator
Accept a function for “my_decorator“, named “func“.
In the “my_decorator” function create another function named “inner_func” (can be any name).
In the “inner_func” call the function “func” that we sent to the “my_decorator“.
Add some steps before and/or after the “func” function call. Here we have added some print statements for demo purposes.
def my_decorator(func):
    def inner_func():
        print("This is some additional processing in the dcorator")
        
        func()
        
        print("Again some additional processing")
    
    return inner_func


def big_box_process():
    print("running my Big Box process")
    

# Demo usage
if __name__ == '__main__':
    decorator_obj = my_decorator(big_box_process)
    decorator_obj()
    
    # Or you can use the following line to do that in a single line
    # my_decorator(big_box_process)()
Python

Usage:

Let’s see how can we apply the decorator to our own function.

Now create a function named “big_box_process” (can be any name). This is our own function.
Call the “my_decorator” function and pass our function name “big_box_process“. This will return a function.
Execute the function that we got from the previous step.

This way we are adding the decorator “my_decorator” to our own function “big_box_process“.

Output:

This is some additional processing in the dcorator

running my simple process

Again some additional processing
Plaintext

Using the Decorator Properly

NOTES

In this step, we will see how we should use the decorator.

We just need to add the “@” sign and then the decorator function name before the function definition, and that will apply the decorator on that function.

Our decorator function definition will be the same, as the next step. We just need to accept a function and return a function at the end of processing.
Before the normal function definition add a “@” sign and then the decorator name, to apply the decorator on that function.
Later, just normally call the function directly, and that will give us the result with the decorator applied.
def my_decorator(func):
    def inner_func():
        print("This is some additional processing in the dcorator")
        
        func()
        
        print("Again some additional processing")
    
    return inner_func
        
@my_decorator
def big_box_process():
    print("running my Big Box process")
    

# Demo usage
if __name__ == '__main__':
    big_box_process()
Python

Output:

This is some additional processing in the dcorator

running my Big Box process

Again some additional processing
Plaintext

Pass Arguments to Function

Accept the arguments in the wrapper method inside the decorator.
Then we can use the values inside the wrapper function.
def my_decorator(func):
    def inner_func(name: str, value: int):
        print("This is some additional processing in the dcorator")
        
        print(f"Using the name and value inside inner_func- name is {name} and value is {value}")
        
        # call the provided function
        func(name, value)
        
        print("Again some additional processing")
    
    return inner_func
        
@my_decorator
def big_box_process(name: str, value: int):
    print("running my Big Box process")
    print("name: ", name)
    print("value: ", value)
    

# Demo usage
if __name__ == '__main__':
    big_box_process("abc", 100)
Python

Output:

This is some additional processing in the dcorator
Using the name and value inside inner_func- name is abc and value is 100

running my Big Box process
name:  abc
value:  100

Again some additional processing
Plaintext

Pass Argument Effectively

To pass the arguments effectively, use the following arguments for the inner function inside the decorator.

*args : to accept positional arguments.
**kwargs : to accept the keyword arguments.

Also pass the same arguments to the function(that applies the decorator), when you want to use it inside the decorator.

def my_decorator(func):
    def inner_func(*args, **kwargs):
        print("This is some additional processing in the dcorator")

        print(f"Using the **args inside inner_func- {args}")
        print(f"Using the **kwargs inside inner_func- {kwargs}")

        # call the provided function
        func(*args, **kwargs)

        print("Again some additional processing")

    return inner_func


@my_decorator
def big_box_process(name: str, value: int):
    print("running my Big Box process")
    print("name: ", name)
    print("value: ", value)


# Demo usage
if __name__ == "__main__":
    big_box_process("abc", value=100)
Python

Output:

This is some additional processing in the dcorator
Using the **args inside inner_func- ('abc',)
Using the **kwargs inside inner_func- {'value': 100}

running my Big Box process
name:  abc
value:  100

Again some additional processing
Plaintext

Decorator with Return Value

If our function has a return value then, add a return statement at the end of the wrapper function
import math
import time


# Decorator for time execution
def log_execution_time(func):
    def wrapper(*args, **kwargs):
        start_time = time.time_ns()
        result = func(*args, **kwargs)
        end_time = time.time_ns()

        print(f"Execution time for {func.__name__}: {end_time - start_time:.6f} ns")

        return result # return at the end of the wrapper 

    return wrapper


# Calculate circle area
@log_execution_time
def circle_area(radius):
    if radius < 0:
        raise ValueError("Radius must be a non-negative.")
    return math.pi * (radius**2)


# Calculate rectangle area
@log_execution_time
def rectangle_area(length, width):
    if length < 0 or width < 0:
        raise ValueError("Length and width must be non-negative.")
    return length * width


# Demo usage
if __name__ == "__main__":
    try:
        print("Circle Area:", circle_area(10))
        print("Rectangle Area:", rectangle_area(5, 12))
    except ValueError as e:
        print(e)
Python

Output:

Execution time for circle_area: 2601.000000 ns
Circle Area: 314.1592653589793

Execution time for rectangle_area: 739.000000 ns
Rectangle Area: 60
Plaintext

Pass Argument to Decorator

If the decorator needs to accept one or more param, then wrap the whole decorator inside a function that returns the decorator at the end.

def font_size(size: int):
    def decorator(func):
        def wrapper(*args, **kwargs):
            return f'<span style="font-size: {size}px;">{func(*args, **kwargs)}</span>'

        return wrapper

    return decorator


# Demo usage
@font_size(24)
def sample_output():
    return "Welcome to BigBoxCode"


print(sample_output())
Python

Output:

<span style="font-size: 24px;">Welcome to BigBoxCode</span>
Plaintext

Chain Multiple Decorators

We can apply multiple decorators to the same function, just write the decorators one by one.

NOTES

The decorator that is applied first(at the top) will process first, and hold the rest of the implementation inside it.

def first_decorator(func):
    def wrapper(*args, **kwargs):
        print("Starting first_decorator")

        func(*args, **kwargs)

        print("Ending first_decorator")

    return wrapper


def second_decorator(func):
    def wrapper(*args, **kwargs):
        print("Starting second_decorator")

        func(*args, **kwargs)

        print("Ending second_decorator")

    return wrapper


# Usage
@first_decorator
@second_decorator
def big_box_process():
    print("Executing big_box_process")


big_box_process()
Python

Output:

Starting first_decorator

Starting second_decorator

Executing big_box_process

Ending second_decorator

Ending first_decorator
Plaintext

Access Function Metadata in Decorator

We can access the function meta data like __name__, __doc__,etc inside the decorator.

from functools import wraps
from typing import Callable


def my_decorator(func: Callable) -> Callable:
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function [inside decorator]: {func.__name__}")
        print(f"Docstring [inside decorator]: {func.__doc__}")
        print(f"Annotations [inside decorator]: {func.__annotations__}")
        result = func(*args, **kwargs)
        return result

    return wrapper


# Usage
@my_decorator
def big_box_process(a: int, b: int) -> int:
    "My implementation for big box process execution"
    return a + b


# Call the function
result = big_box_process(5, 3)
print(f"Result: {result}")

# Access metadata directly
print(f"Function Name: {big_box_process.__name__}")
print(f"Docstring: {big_box_process.__doc__}")
print(f"Annotations: {big_box_process.__annotations__}")
Python

Output:

Calling function [inside decorator]: big_box_process
Docstring [inside decorator]: My implementation for big box process execution
Annotations [inside decorator]: {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

Result: 8
Function Name: big_box_process
Docstring: My implementation for big box process execution
Annotations: {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
Plaintext

Modify Function Metadata in Decorator

We can also change or add metadata inside the decorator.

from functools import wraps
from typing import Callable


def my_custom_metadata(key: str, value: str) -> Callable:
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)

        # Add the custom metadata to the function
        setattr(wrapper, key, value)
        return wrapper

    return decorator


# Usage
@my_custom_metadata("custom_metadata", "This is a custom metadata for function.")
def big_box_process(a: int, b: int) -> int:
    "My implementation for big box process execution"
    return a + b


# Call the function
result = big_box_process(5, 3)
print(f"Result: {result}")

# Access metadata
print(f"Function Name: {big_box_process.__name__}")
print(f"Docstring: {big_box_process.__doc__}")
print(f"Annotations: {big_box_process.__annotations__}")
print(f"Custom Metadata: {getattr(big_box_process, 'custom_metadata', 'Not Found')}")
Python

Output:

Result: 8
Function Name: big_box_process
Docstring: My implementation for big box process execution
Annotations: {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
Custom Metadata: This is a custom metadata for function.
Plaintext

Protect Function Metadata

When the function is passed to the decorator, the decorator creates a new function and returns that. So the metadata of the original function is no longer reserved, contained in the returned function form decorator.

Let’s see, what that means, and how we can preserve the original metadata of a function inside the decorator.

Without Metadata Protection

When we apply a decorator to a functional check of the metadata, it returns the metadata of wrapper function(not of the original function).

def my_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def big_box_process(val: int) -> str:
    """My big box process implementation"""
    
    return f"Provided value: {val}!"

print(big_box_process.__name__)
print(big_box_process.__doc__)
print(big_box_process.__annotations__)
Python

Output:

wrapper
None
{}
Plaintext

With Metadata Protection

Apply decorator “@functools.wraps()” to preserve the metadata of the original function.

from functools import wraps


def my_decorator(func):
    @wraps(func) # Use this to prserve the metadata of original function
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@my_decorator
def big_box_process(val: int) -> str:
    """My big box process implementation"""

    return f"Provided value: {val}!"


print(big_box_process.__name__)
print(big_box_process.__doc__)
print(big_box_process.__annotations__)
Python

Output:

big_box_process
My big box process implementation
{'val': <class 'int'>, 'return': <class 'str'>}
Plaintext

Class Decorator

Here is an example of class decorator-

from functools import wraps


# Class decorator
def my_class_decorator(cls):
    # Add a new class attribute
    cls.new_attribute = "This is a new attribute"

    # Add a new method to the class
    def new_method(self):
        return f"New method called. Original attribute: {self.original_attribute}"

    setattr(cls, "new_method", new_method)

    # Decorate existing methods
    for attr_name, attr_value in list(cls.__dict__.items()):
        if callable(attr_value) and not attr_name.startswith("__"):
            setattr(cls, attr_name, log_method(attr_value))

    return cls


# Method decorator to log calls
def log_method(method):
    @wraps(method)
    def wrapper(*args, **kwargs):
        print(
            f"Calling method: {method.__name__} with args: {args[1:]}, kwargs: {kwargs}"
        )
        return method(*args, **kwargs)

    return wrapper


# Applying the class decorator
@my_class_decorator
class MyClass:
    def __init__(self, original_attribute):
        self.original_attribute = original_attribute

    def operation1(self, name: str):
        """Greet a person."""
        return f"Hello, {name}! Your attribute is {self.original_attribute}"

    def operation2(self, a: int, b: int) -> int:
        """Adds two numbers."""
        return a + b


# Using the decorated class
obj = MyClass("Decorated Class")

# Accessing original and added attributes
print(f"Original Attribute: {obj.original_attribute}")
print(f"New Attribute: {MyClass.new_attribute}")

# Calling original methods (now logged)
print(obj.operation1("BigBoxCode"))
print(obj.operation2(10, 20))

# Calling the new method added by the decorator
print(obj.new_method())
Python

Output:

Original Attribute: Decorated Class
New Attribute: This is a new attribute

Calling method: operation1 with args: ('BigBoxCode',), kwargs: {}
Hello, BigBoxCode! Your attribute is Decorated Class

Calling method: operation2 with args: (10, 20), kwargs: {}
30

Calling method: new_method with args: (), kwargs: {}
New method called. Original attribute: Decorated Class
Plaintext

Examples [Custom Decorators]

Here are some examples of how we can define and use decorators-

Example #1: Uppercase Decorator

from typing import Callable


def uppercase(func: Callable[..., str]) -> Callable[..., str]:
    def wrapper(*args: object, **kwargs: object) -> str:
        result = func(*args, **kwargs)
        return result.upper()

    return wrapper


@uppercase
def user_greeting(name: str) -> str:
    return f"Hello, {name}"


print(user_greeting("John Doe"))
Python

Output:

HELLO, JOHN DOE
Plaintext

Example #2: Apply HTML Tags(chaining decorators)

def bold(func):
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"

    return wrapper


def italic(func):
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"

    return wrapper


def font_size(size: int):
    def decorator(func):
        def wrapper(*args, **kwargs):
            return f'<span style="font-size: {size}px;">{func(*args, **kwargs)}</span>'

        return wrapper

    return decorator


# Demo usage
@bold
@italic
@font_size(24)
def sample_output():
    return "Welcome to BigBoxCode"


print(sample_output())
Python

Output:

<b><i><span style="font-size: 24px;">Welcome to BigBoxCode</span></i></b>
Plaintext

Example #3: State Management

from functools import wraps
from typing import Callable, Any


def memoize(func: Callable) -> Callable:
    cache = {}

    @wraps(func)
    def wrapper(*args, **kwargs) -> Any:
        # Create a key from the function arguments
        key = (args, frozenset(kwargs.items()))
        
        print(f"Cache Key: {key}")
        
        if key not in cache:
            print(f"Cache miss. calculating...")
            cache[key] = func(*args, **kwargs)
        else:
            print(f"Cache hit.")

        return cache[key]

    # Expose if required(for debugging or any other reason)
    wrapper.cache = cache

    return wrapper


# Usage
@memoize
def big_box_process(x: int, y: int) -> int:
    """Simulates an expensive computation."""
    return x**y


# Function calls
print(big_box_process(2, 10))
print(big_box_process(2, 10))
print(big_box_process(3, 5))
print(big_box_process(3, 5))

# Inspecting the cache
print("Cache contents:", big_box_process.cache)
Python

Output:

Cache Key: ((2, 10), frozenset())
Cache miss. calculating...
1024

Cache Key: ((2, 10), frozenset())
Cache hit.
1024

Cache Key: ((3, 5), frozenset())
Cache miss. calculating...
243

Cache Key: ((3, 5), frozenset())
Cache hit.
243

Cache contents: {((2, 10), frozenset()): 1024, ((3, 5), frozenset()): 243}
Plaintext

Example #4: Request Router

class App:
    routes = {}

    # Router decorator
    def route(self, path):
        def decorator(func):

            # Perform some checking in the decorator
            if not self.check_auth():
                return "403 Forbidden: Unauthorized"

            # Function to just wrap the full request
            # Not directly related to decorator
            def request_wrapper(*args, **kwargs):
                response = func(*args, **kwargs)

                headers = {"X-Customer-Id": "100"}

                return f"Headers: {headers}\n{response}"

            # Register the wrapped function
            self.routes[path] = request_wrapper
            return request_wrapper

        return decorator

    def check_auth(self):
        # Dummy authentication
        # Returing true for dummy purpose
        return True
    
    def handle_reqeust(self, path):
        if path in self.routes:
            return self.routes[path]()
        else:
            return "404 Not Found"


# Demo usage
app = App()


@app.route("/")
def home_page():
    return "bigboxcode home"


@app.route("/contact")
def contact_page():
    return "Contact us at support@bigboxcode.com"


# handle request when it comes from the user
print(app.handle_reqeust("/"))
print(app.handle_reqeust("/contact"))
print(app.handle_reqeust("/sone-unknown-path"))
Python

Output:

Headers: {'X-Customer-Id': '100'}
bigboxcode home

Headers: {'X-Customer-Id': '100'}
Contact us at support@bigboxcode.com

404 Not Found
Plaintext

Builtin Decorators [Commonly Used]

@staticmethod

class BigBoxOps:
    @staticmethod
    def my_static(a: int, b: int) -> int:
        return a + b


# Demo usage
result = BigBoxOps.my_static(2, 3)

print(result)
Python

Output:

5
Plaintext

@classmethod

class BigBoxOps:
    no_of_days: int = 7

    @classmethod
    def get_no_of_days(self) -> int:
        return self.no_of_days


# Demo usage
result = BigBoxOps.get_no_of_days()

print(result)
Python

Output:

7
Plaintext

@property

class Rectangle:
    def __init__(self, height: float, width: float) -> None:
        self.__height = height
        self.__width = width

    @property
    def height(self) -> float:
        return self.__height

    @property
    def width(self) -> float:
        return self.__width

    @property
    def area(self) -> float:
        return self.__height * self.__width


# demo usage
rectangle = Rectangle(50, 100)

print(f"Height: {rectangle.height}")
print(f"Width: {rectangle.width}")
print(f"Area: {rectangle.area}")
Python

Output:

Height: 50
Width: 100
Area: 5000
Plaintext

@abstractmethod

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass


class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height


# Demo usage
rect = Rectangle(4, 5)
print(rect.area())
Python

Output:

20
Plaintext

@functools.lru_cache

from functools import lru_cache


@lru_cache(maxsize=None)
def slow_function(x: float) -> float:
    """This simulates some slow functions"""

    print(f"Cache not found for {x}. Calculating...")

    return x * x


# Demo usage
print(f"Slow function result for 4: {slow_function(4)}")
print(f"Slow function result for 4: {slow_function(4)}")
print(f"Slow function result for 5: {slow_function(5)}")
print(f"Slow function result for 4: {slow_function(4)}")
print(f"Slow function result for 5: {slow_function(5)}")
print(f"Slow function result for 6: {slow_function(6)}")
print(f"Slow function result for 6: {slow_function(6)}")
Python

Output:

Cache not found for 4. Calculating...
Slow function result for 4: 16
Slow function result for 4: 16
Cache not found for 5. Calculating...
Slow function result for 5: 25
Slow function result for 4: 16
Slow function result for 5: 25
Cache not found for 6. Calculating...
Slow function result for 6: 36
Slow function result for 6: 36
Plaintext

@functools.wraps

from functools import wraps


def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@my_decorator
def my_func(name):
    """Sample function"""

    return f"Hello, {name}"

# Demo usage
print(my_func.__name__)
print(my_func.__doc__)
Python

Output:

my_func
Sample function
Plaintext

@contextlib.contextmanager

import os
import tempfile
from contextlib import contextmanager


@contextmanager
def temporary_file():
    """
    Context manager for creating and automatically cleaning up a temporary file.
    """
    temp_file = tempfile.NamedTemporaryFile(delete=False)
    try:
        print(f"Created temporary file: {temp_file.name}")
        yield temp_file.name
    finally:
        try:
            os.remove(temp_file.name)
            print(f"Deleted temporary file: {temp_file.name}")
        except FileNotFoundError:
            print("File already deleted.")


# Demo usage
with temporary_file() as file_path:
    print(f"Working with temporary file: {file_path}")

    with open(file_path, "w") as f:
        f.write("Hello, this is a temporary file!")

    # File exists at this point
    with open(file_path, "r") as f:
        print(f.read())
Python

Output:

Created temporary file: /tmp/tmppvhgz_vi
Working with temporary file: /tmp/tmppvhgz_vi
Hello, this is a temporary file!
Deleted temporary file: /tmp/tmppvhgz_vi
Plaintext

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.