Decorators with Arguments

Welcome to this detailed exploration of decorators with arguments in Python! If you’ve worked with simple decorators before, you know they’re a powerful way to extend or modify the behavior of functions or methods without changing their code. But what happens when you want your decorator to be customizable, to accept parameters that influence how it behaves? That’s where decorators with arguments come in.

In this lesson, we'll unravel the concept step-by-step, demystify the syntax, and build up practical examples to make decorators with arguments feel like second nature. By the end, you’ll be able to design your own flexible decorators, enhancing your Python code with reusable and configurable functionality.

Why Use Decorators with Arguments?

Imagine you want to create a decorator that logs function calls. A simple decorator might just print a message every time the function runs. But what if you want to specify the log level — like INFO, DEBUG, or WARNING — when applying the decorator? Or perhaps you want to enable or disable the logging dynamically?

Using decorators with arguments lets you pass such parameters directly to the decorator, tailoring its behavior on a per-use basis:

  • Control how the decorator behaves.
  • Reuse the same decorator logic with different configurations.
  • Make your code more expressive and maintainable.

💡 Key Insight

Think of a decorator with arguments like a factory: it produces a decorator customized with the parameters you provide. It's a three-layer function setup rather than the usual two-layer for simple decorators.

Recap: Simple Decorators (Without Arguments)

Before diving into arguments, let's quickly recall what a simple decorator looks like. A decorator is a function that takes another function as input, and returns a modified or wrapped function.

📌 Deep Dive: Simple Decorator Example

PYTHON
def simple_decorator(func):
    def wrapper():
        print("Before the function runs")
        func()
        print("After the function runs")
    return wrapper

@simple_decorator
def greet():
    print("Hello!")

greet()
Output
Before the function runs Hello! After the function runs

Here, @simple_decorator replaces greet with the wrapper function inside simple_decorator. This is straightforward, but what if you want to pass parameters to simple_decorator?

Understanding the Structure of Decorators with Arguments

A decorator with arguments adds an extra layer of functions. Instead of one function wrapping another, you have three:

  1. Decorator factory: This outermost function accepts arguments.
  2. Actual decorator: This middle function takes the function to decorate.
  3. Wrapper function: This innermost function replaces the original function and adds behavior.

The outermost function returns the actual decorator, which in turn returns the wrapper. The wrapper is what runs when you call the decorated function.

Architecture of Decorators with Arguments
Architecture of Decorators with Arguments

💡 Visualizing the Flow

When you write @decorator(arg), Python first calls decorator(arg), which returns a function that gets applied as the actual decorator.

Building a Decorator with Arguments: Step by Step

Let’s build a simple decorator that repeats the execution of a function a specified number of times — the number will be passed as an argument to the decorator.

📌 Deep Dive: Repeat Decorator with Arguments

PYTHON
def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

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

say_hello()
Output
Hello! Hello! Hello!

Here’s what happens under the hood:

  • repeat(3) is called first, returning the decorator_repeat function with num_times=3 bound.
  • @decorator_repeat is then applied to say_hello, returning the wrapper function.
  • When say_hello() is called, the wrapper executes the original say_hello function 3 times.

Handling Arguments and Return Values

A good decorator should be able to handle any number of positional and keyword arguments, and return the original function’s result properly. Notice the use of *args and **kwargs in the wrapper. This ensures that the wrapper can accept any input signature and pass it correctly to the original function.

Also, if the decorated function returns a value, the decorator should return it as well. In our example, we return the last call’s result.

⚠️ Important

Always use *args and **kwargs in your wrapper functions to make decorators flexible and compatible with any function signature.

Preserving Function Metadata with functools.wraps

When you wrap a function, Python replaces the original function’s metadata — like its name, docstring, and annotations — with the wrapper’s. This can cause issues, especially with debugging or documentation tools.

To preserve this metadata, use the functools.wraps decorator inside your wrapper function:

📌 Deep Dive: Using functools.wraps

PYTHON
import functools

def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(2)
def greet(name):
    """Greet a person by name."""
    print(f"Hello, {name}!")

greet("Alice")
print(greet.__name__)
print(greet.__doc__)
Output
Hello, Alice! Hello, Alice! greet Greet a person by name.

Common Patterns and Use Cases for Decorators with Arguments

Decorators with arguments are widely used in many Python libraries and frameworks. Here are some examples:

  • Logging Decorators that accept log levels or formats.
  • Authorization Decorators that accept roles or permission levels.
  • Retry Decorators that accept retry counts and delays.
  • Timing Decorators that can enable/disable timing or specify output details.

They empower reusable, configurable enhancements in a clean and Pythonic way.

Simple Decorator vs. Decorator with Arguments
AspectSimple DecoratorDecorator with Arguments
Function Layers2 (decorator + wrapper)3 (factory + decorator + wrapper)
Usage Syntax@decorator@decorator(args)
ParameterizationNo parameters allowedAccepts parameters to change behavior
Example UseSimple timing, loggingLogging with log level, retries with count

Example: Logging Decorator with Level Argument

Let’s create a logging decorator that prints messages differently based on a log level argument.

📌 Deep Dive: Logging Decorator

PYTHON
import functools

def log(level):
    def decorator_log(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{level}] - Calling {func.__name__}")
            result = func(*args, **kwargs)
            print(f"[{level}] - {func.__name__} completed")
            return result
        return wrapper
    return decorator_log

@log("INFO")
def add(x, y):
    return x + y

@log("DEBUG")
def multiply(x, y):
    return x * y

print(add(3, 5))
print(multiply(4, 6))
Output
[INFO] - Calling add [INFO] - add completed 8 [DEBUG] - Calling multiply [DEBUG] - multiply completed 24

Tips for Writing Your Own Decorators with Arguments

  • Start from the outside in: first write the decorator factory that accepts arguments.
  • Use *args and **kwargs in wrappers to support any function signature.
  • Always use functools.wraps to preserve metadata.
  • Test your decorators with different functions and argument types to ensure flexibility.
  • Keep it readable: complex decorators can be hard to debug, so write clear docstrings and comments.

💡 Remember

Decorators with arguments are a powerful way to customize behavior without cluttering your function code. They encourage separation of concerns and cleaner, more expressive code structures.

Summary

Decorators with arguments unlock a higher level of flexibility compared to simple decorators. By wrapping your decorator in a factory function, you can pass parameters that control how your decorator modifies function behavior.

Key takeaways:

  • They require three nested functions: factory, decorator, and wrapper.
  • Use *args and **kwargs to handle arbitrary arguments.
  • Use functools.wraps to preserve original function metadata.
  • They are widely applicable in logging, authorization, retry logic, and more.

Practice writing your own decorators with arguments to unlock powerful patterns in your Python projects.