What are Python Decorators?

A decorator in Python is a higher-order function that allows you to modify or extend the behavior of another function without changing its actual code. Decorators are widely used in logging, authentication, caching, and other programming tasks.

Why Use Python Decorators?

Enhances Functionality – Modify functions dynamically
Code Reusability – Write reusable wrappers for multiple functions
Keeps Code Clean – Avoids repetitive logic in multiple places

Basic Syntax of a Decorator

def decorator_function(original_function):
    def wrapper_function():
        print("Wrapper executed before", original_function.__name__)
        return original_function()
    return wrapper_function

Step-by-Step Guide with Examples

1. Creating a Simple Decorator

Example: A Basic Decorator That Logs Function Execution

def log_decorator(func):
    def wrapper():
        print(f"Calling function: {func.__name__}")
        func()
        print(f"Function {func.__name__} executed successfully")
    return wrapper

@log_decorator
def say_hello():
    print("Hello, World!")

say_hello()

Output:

Calling function: say_hello Hello, World! Function say_hello executed successfully

💡 Best Practice: Always use meaningful decorator names that describe their purpose.

2. Using Decorators with Arguments

Example: Decorator for Timing Function Execution

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} executed in {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@timing_decorator
def slow_function():
    time.sleep(2)
    print("Function completed")

slow_function()

Output:

Function completed slow_function executed in 2.0001 seconds

💡 Best Practice: Use *args and **kwargs in the wrapper function to handle functions with arguments.

3. Applying Multiple Decorators

Example: Combining Logging and Execution Timing Decorators

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Executing {func.__name__}()")
        return func(*args, **kwargs)
    return wrapper

def timing_decorator(func):
    import time
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__}() took {end - start:.4f} seconds")
        return result
    return wrapper

@log_decorator
@timing_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

Output:

 
Executing greet() Hello, Alice! greet() took 0.0001 seconds

💡 Best Practice: The order of decorators matters. The decorator closest to the function executes first.

4. Creating a Parameterized Decorator

Example: A Decorator That Accepts Arguments

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def greet():
    print("Hello!")

greet()

Output:

Hello! Hello! Hello!

💡 Best Practice: Use an inner function inside the decorator to handle arguments dynamically.

5. Using functools.wraps to Preserve Function Metadata

By default, decorators modify the function name and docstring. The functools.wraps module preserves the original function’s metadata.

Example:

import functools

def log_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Executing {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_decorator
def add(a, b):
    """Returns the sum of two numbers."""
    return a + b

print(add.__name__)  # Output: add
print(add.__doc__)   # Output: Returns the sum of two numbers.

💡 Best Practice: Always use @functools.wraps(func) in decorators to avoid losing function metadata.

Real-World Applications of Decorators

Logging Function Calls – Automatically log when a function is executed
Caching Results – Store previous results to improve performance
Authentication – Restrict access to functions based on user roles
Performance Monitoring – Measure execution time of functions

When to Use Decorators?

✅ When modifying function behavior without changing the function’s code
✅ When applying reusable wrappers like logging, authentication, or timing
✅ When you need cleaner and more maintainable code

🚫 Avoid using decorators for simple cases where a direct function call works just fine.