Skip to main content

How to Apply Decorators to Methods Correctly in Python

Decorators are a cornerstone of advanced Python programming, allowing you to modify or enhance the behavior of methods dynamically. However, applying decorators to class methods involves nuances that don't exist with standalone functions, such as handling the self argument and preserving metadata.

This guide explains how to write robust method decorators, handle instance context, and stack multiple decorators effectively.

Understanding Method Decorators

A method decorator is simply a function that takes a method as input and returns a new function (wrapper) that typically calls the original method.

Unlike standalone functions, methods are bound to an object instance (self). Your wrapper function must accept self (and other arguments) to pass them correctly to the original method.

Handling self and Arguments

The most common pitfall is forgetting to account for self in the wrapper function's signature. The universal pattern uses *args and **kwargs, where self is implicitly captured in args[0] or explicitly defined.

Pattern 1: Explicit self (Cleaner for Methods)

If you know the decorator is only for methods, defining self explicitly makes the code more readable.

def log_method_call(func):
def wrapper(self, *args, **kwargs):
print(f"Calling method '{func.__name__}' on instance {self}")
return func(self, *args, **kwargs)
return wrapper

class Calculator:
@log_method_call
def add(self, x, y):
return x + y

calc = Calculator()
calc.add(5, 3)

Output:

Calling method 'add' on instance <__main__.Calculator object at ...>

Pattern 2: Universal Decorator (Works on Functions too)

If you want the decorator to work on both standalone functions and class methods, treat self as just another argument in *args.

def timer(func):
def wrapper(*args, **kwargs):
print("Starting timer...")
result = func(*args, **kwargs)
print("Timer stopped.")
return result
return wrapper

class Task:
@timer
def run(self):
print("Running task...")

t = Task()
t.run()

Output:

Starting timer...
Running task...
Timer stopped.

Preserving Metadata with functools.wraps

When you wrap a function, the new function (wrapper) loses the original function's name (__name__) and docstring (__doc__). This can break introspection tools and make debugging harder.

Always use @functools.wraps(func) on your wrapper.

import functools

def validate_positive(func):
@functools.wraps(func) # ✅ Correct: Preserves metadata
def wrapper(self, x, y):
if x < 0 or y < 0:
raise ValueError("Inputs must be positive")
return func(self, x, y)
return wrapper

class MathOps:
@validate_positive
def multiply(self, x, y):
"""Multiplies two positive numbers."""
return x * y

# Verify metadata
m = MathOps()
print(f"Method Name: {m.multiply.__name__}")
print(f"Docstring: {m.multiply.__doc__}")

Output:

Method Name: multiply
Docstring: Multiplies two positive numbers.

Decorator Stacking Order

You can apply multiple decorators to a single method. The order matters: decorators are applied from bottom to top (innermost to outermost).

Logic: @Dec1(@Dec2(Method)) -> Dec1 wraps Dec2, which wraps Method.

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

class TextFormatter:
@bold # Executed LAST (Outer layer)
@italic # Executed FIRST (Inner layer)
def format(self, text):
return text

tf = TextFormatter()
print(tf.format("Hello"))

Output:

<b><i>Hello</i></b>
warning

If you swap the order (@italic then @bold), the output becomes <i><b>Hello</b></i>.

Conclusion

To apply decorators to methods correctly:

  1. Accept Arguments: Ensure your wrapper accepts *args and **kwargs to handle self and method parameters seamlessly.
  2. Preserve Metadata: Always use @functools.wraps(func) to keep the original method's name and docstring.
  3. Return Values: Don't forget to return func(...) inside the wrapper, or your method will return None.
  4. Mind the Order: Remember that decorators stack from the bottom up.