Function Decorators

Introduction

A Decorator is a powerful Python feature that allows you to modify or extend the behavior of a function without changing its original code.

Decorators are commonly used for:

  • Logging

  • Authentication

  • Authorization

  • Performance Monitoring

  • Exception Handling

  • Timing Function Execution

  • Automation Framework Development

Decorators follow the principle of “wrapping” a function inside another function.

In this tutorial, you will learn what decorators are, how they work, practical examples, automation testing use cases, common mistakes, and best practices.


What is a Decorator?

A decorator is a function that takes another function as an argument, adds some functionality, and returns a new function.

Basic Syntax

def decorator_function(original_function):

    def wrapper():
        print("Before function execution")

        original_function()

        print("After function execution")

    return wrapper

Why Use Decorators?

Decorators help:

  • Reuse code

  • Avoid code duplication

  • Add functionality dynamically

  • Improve code readability

  • Follow the DRY (Don’t Repeat Yourself) principle


Functions are First-Class Objects

In Python, functions can be:

  • Assigned to variables

  • Passed as arguments

  • Returned from other functions

Example

def greet():
    print("Hello")

message = greet

message()

Output

Hello

This concept makes decorators possible.


Creating a Simple Decorator

Example

def my_decorator(func):

    def wrapper():
        print("Before execution")

        func()

        print("After execution")

    return wrapper


def display():
    print("Display Function")

decorated_function = my_decorator(display)

decorated_function()

Output

Before execution
Display Function
After execution

Using @ Decorator Syntax

Python provides a cleaner syntax.

Example

def my_decorator(func):

    def wrapper():
        print("Before execution")

        func()

        print("After execution")

    return wrapper


@my_decorator
def display():
    print("Display Function")

display()

Output

Before execution
Display Function
After execution

This is equivalent to:

display = my_decorator(display)

Decorator with Function Arguments

Example

def decorator(func):

    def wrapper(name):
        print("Welcome")

        func(name)

        print("Thank You")

    return wrapper


@decorator
def greet(name):
    print(f"Hello {name}")

greet("John")

Output

Welcome
Hello John
Thank You

Decorator Using *args and **kwargs

This makes decorators work with any number of arguments.

Example

def decorator(func):

    def wrapper(*args, **kwargs):

        print("Function Started")

        func(*args, **kwargs)

        print("Function Ended")

    return wrapper


@decorator
def add(a, b):
    print(a + b)

add(10, 20)

Output

30
Function Ended

Actually output:

Function Started
30
Function Ended

Decorator Returning Values

Example

def decorator(func):

    def wrapper(*args, **kwargs):

        result = func(*args, **kwargs)

        return result

    return wrapper


@decorator
def multiply(a, b):
    return a * b

print(multiply(5, 4))

Output

20

Logging Decorator

Example

def logger(func):

    def wrapper(*args, **kwargs):

        print(
            f"Executing {func.__name__}"
        )

        return func(*args, **kwargs)

    return wrapper


@logger
def login():
    print("User Logged In")

login()

Output

Executing login
User Logged In

Execution Time Decorator

Example

import time

def timer(func):

    def wrapper(*args, **kwargs):

        start = time.time()

        result = func(*args, **kwargs)

        end = time.time()

        print(
            f"Execution Time: {end-start}"
        )

        return result

    return wrapper


@timer
def process():

    time.sleep(2)

process()

Output

Execution Time: 2.00...

Authentication Decorator

Example

def authenticate(func):

    def wrapper(user):

        if user == "admin":
            return func(user)

        print("Access Denied")

    return wrapper


@authenticate
def dashboard(user):
    print("Welcome Admin")

dashboard("admin")

Output

Welcome Admin

Multiple Decorators

Example

def uppercase(func):

    def wrapper():
        return func().upper()

    return wrapper


def add_stars(func):

    def wrapper():
        return f"*** {func()} ***"

    return wrapper


@add_stars
@uppercase
def message():
    return "hello"

print(message())

Output

*** HELLO ***

Preserving Function Metadata

Without precautions, decorators can change a function’s name and documentation.

Use functools.wraps().

Example

from functools import wraps

def decorator(func):

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

        return func(*args, **kwargs)

    return wrapper

Practical Example: Exception Handling Decorator

Example

def handle_exception(func):

    def wrapper(*args, **kwargs):

        try:
            return func(*args, **kwargs)

        except Exception as e:
            print(f"Error: {e}")

    return wrapper


@handle_exception
def divide(a, b):
    return a / b

print(divide(10, 0))

Output

Error: division by zero
None

Decorators in Selenium Automation

Example

Capture test execution logs.

def log_test(func):

    def wrapper():

        print(
            f"Running {func.__name__}"
        )

        func()

        print(
            f"Completed {func.__name__}"
        )

    return wrapper


@log_test
def login_test():
    print("Executing Login Test")

login_test()

Output

Running login_test
Executing Login Test
Completed login_test

Decorators in API Automation

Example

Log API requests.

def api_logger(func):

    def wrapper(*args, **kwargs):

        print("Sending API Request")

        response = func(*args, **kwargs)

        print("Response Received")

        return response

    return wrapper


@api_logger
def get_users():
    return {"status": 200}

print(get_users())

Output

Sending API Request
Response Received
{'status': 200}

Common Mistakes Beginners Make

Forgetting to Return Wrapper

Incorrect

def decorator(func):

    def wrapper():
        pass

Decorator won’t work properly.


Correct

def decorator(func):

    def wrapper():
        pass

    return wrapper

Forgetting *args and **kwargs

Incorrect

def wrapper():

May fail for functions with parameters.


Correct

def wrapper(*args, **kwargs):

Not Returning Function Result

Incorrect

func(*args, **kwargs)

Result is lost.


Correct

return func(*args, **kwargs)

Forgetting @wraps

Without it:

print(func.__name__)

May show:

wrapper

instead of the original function name.


Best Practices

Use functools.wraps()

from functools import wraps

Keep Decorators Focused

One decorator should perform one task.

Examples:

  • Logging

  • Authentication

  • Timing


Use *args and **kwargs

Ensures flexibility.


Return Results Properly

return result

Avoid Excessive Nesting

Too many decorators can make debugging difficult.


Advantages of Decorators

  • Reusable code

  • Cleaner functions

  • Better maintainability

  • Reduced duplication

  • Powerful customization


Limitations of Decorators

  • Can be difficult for beginners

  • Excessive nesting may reduce readability

  • Debugging can become complex


Decorator vs Normal Function

Feature Decorator Normal Function
Modifies Other Functions Yes No
Reusable High Moderate
Supports Wrapping Yes No
Code Duplication Reduction Yes No

Conclusion

Function decorators are one of Python’s most elegant features for extending function behavior without modifying existing code. They help keep code clean, reusable, and maintainable.

Whether you’re adding logging, timing, authentication, exception handling, or automation framework utilities, decorators provide a powerful way to apply functionality consistently across multiple functions.

Mastering decorators is an essential skill for intermediate and advanced Python developers, especially those building Selenium, API, and automation frameworks.


Frequently Asked Questions (FAQs)

What is a decorator in Python?

A decorator is a function that modifies the behavior of another function.


What does @decorator_name mean?

It is shorthand for:

function = decorator_name(function)

Why use *args and **kwargs in decorators?

They allow decorators to work with functions having any number of arguments.


What is functools.wraps() used for?

It preserves the original function’s metadata such as name and docstring.


Can multiple decorators be applied?

Yes.

@decorator1
@decorator2
def my_function():
    pass

Key Takeaways

  • Decorators modify function behavior without changing original code.

  • Decorators are functions that accept other functions as arguments.

  • The @ syntax provides a clean way to apply decorators.

  • *args and **kwargs make decorators flexible.

  • functools.wraps() preserves metadata.

  • Common uses include logging, timing, authentication, and exception handling.

  • Decorators reduce code duplication.

  • Widely used in Selenium and API automation frameworks.

  • Multiple decorators can be stacked.

  • Decorators are an important feature for writing clean and maintainable Python code.