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
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()
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:
- Decorator factory: This outermost function accepts arguments.
- Actual decorator: This middle function takes the function to decorate.
- 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.

💡 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
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()
Here’s what happens under the hood:
repeat(3)is called first, returning thedecorator_repeatfunction withnum_times=3bound.@decorator_repeatis then applied tosay_hello, returning thewrapperfunction.- When
say_hello()is called, thewrapperexecutes the originalsay_hellofunction 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
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__)
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.
| Aspect | Simple Decorator | Decorator with Arguments |
|---|---|---|
| Function Layers | 2 (decorator + wrapper) | 3 (factory + decorator + wrapper) |
| Usage Syntax | @decorator | @decorator(args) |
| Parameterization | No parameters allowed | Accepts parameters to change behavior |
| Example Use | Simple timing, logging | Logging 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
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))
Tips for Writing Your Own Decorators with Arguments
- Start from the outside in: first write the decorator factory that accepts arguments.
- Use
*argsand**kwargsin wrappers to support any function signature. - Always use
functools.wrapsto 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
*argsand**kwargsto handle arbitrary arguments. - Use
functools.wrapsto 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.
Quick Knowledge Check
Test what you just learned
Question 1 of 2
How many nested functions are typically involved in a decorator with arguments?
Question 2 of 2
What is the purpose of using functools.wraps inside a decorator?
Loading results...