python
class-methods
decorators
self-argument
programming-tips

Class method decorator with self arguments?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Python decorators often confuse people when methods are involved because self is not passed to the decorator at definition time. The decorator receives the function object, and only later, when the method is called on an instance, does the wrapper receive self. Once you separate those two moments, method decorators become much easier to reason about.

Decorators Wrap Functions First

A decorator is applied when the class body is executed, before any instance exists. That means this function:

python
1def log_calls(fn):
2    def wrapper(*args, **kwargs):
3        print("calling", fn.__name__)
4        return fn(*args, **kwargs)
5    return wrapper

receives the undecorated function object, not an instance and not self.

When you use it on an instance method:

python
1class Greeter:
2    @log_calls
3    def hello(self, name):
4        return f"Hello, {name}"

the wrapper gets called later with self as the first positional argument because that is how instance methods work in Python.

Accessing self in the Wrapper

If you need the instance, read it from the wrapper arguments.

python
1from functools import wraps
2
3
4def log_instance_call(fn):
5    @wraps(fn)
6    def wrapper(self, *args, **kwargs):
7        print(f"{self.__class__.__name__}.{fn.__name__} called")
8        return fn(self, *args, **kwargs)
9    return wrapper
10
11
12class Greeter:
13    @log_instance_call
14    def hello(self, name):
15        return f"Hello, {name}"
16
17
18g = Greeter()
19print(g.hello("Ada"))

Here self is not a special decorator feature. It is just the first argument passed when the bound method is invoked.

Decorator Factories When You Need Parameters

If the decorator itself needs configuration, use a decorator factory.

python
1from functools import wraps
2
3
4def require_role(role_name):
5    def decorator(fn):
6        @wraps(fn)
7        def wrapper(self, *args, **kwargs):
8            if getattr(self, "role", None) != role_name:
9                raise PermissionError("wrong role")
10            return fn(self, *args, **kwargs)
11        return wrapper
12    return decorator
13
14
15class Service:
16    def __init__(self, role):
17        self.role = role
18
19    @require_role("admin")
20    def reset(self):
21        return "reset complete"

The outer function receives the decorator configuration. The inner wrapper receives self when the method is called.

@classmethod Changes the First Argument

A class method does not receive self. It receives cls, the class object.

python
1from functools import wraps
2
3
4def log_class_call(fn):
5    @wraps(fn)
6    def wrapper(cls, *args, **kwargs):
7        print(f"class call on {cls.__name__}")
8        return fn(cls, *args, **kwargs)
9    return wrapper
10
11
12class Example:
13    @classmethod
14    @log_class_call
15    def build(cls, value):
16        return cls()

The order matters. The decorator wrapping the plain function and @classmethod converting it into a class method are distinct steps.

If you reverse the order, you are decorating a classmethod descriptor object instead of the underlying function, which usually is not what you want.

Instance Methods, Class Methods, and Static Methods

Keep the first argument model straight:

  • instance method: first argument is self
  • class method: first argument is cls
  • static method: no automatic first argument

That is why a decorator written for instance methods does not automatically fit a static method or class method. The wrapper signature must match how the decorated callable will be invoked.

Preserve Metadata with wraps

Always use functools.wraps unless you have a strong reason not to.

python
from functools import wraps

Without it, the wrapped method loses metadata such as:

  • '__name__'
  • '__doc__'
  • inspection friendliness

This becomes painful in debugging, testing, and frameworks that inspect callables.

Common Pitfalls

  • Expecting self to be available when the decorator itself is created.
  • Forgetting that the wrapper, not the decorator factory, receives self.
  • Writing one wrapper signature and assuming it works for instance, class, and static methods.
  • Applying @classmethod and a custom decorator in the wrong order.
  • Omitting @wraps and losing method metadata.

Summary

  • Decorators receive the function object at definition time, not self.
  • For instance methods, self appears later as the first wrapper argument.
  • For class methods, the first wrapper argument is cls.
  • Use a decorator factory when the decorator needs configuration.
  • Keep method type and decorator order straight, especially around @classmethod.

Course illustration
Course illustration

All Rights Reserved.