skip to content

How does `self` reach the wrapper of a decorated instance method?

level: juniorimportance: must knowfreq 50%

answer

  1. Decoration happens before any binding
  2. The class attribute is now the wrapper
  3. Functions are descriptors, so wrappers bind too
  4. The instance arrives as args[0]

basics

~20 s

Decoration runs in the class body, on the plain function, before any binding exists. The class attribute is now the wrapper, so attribute lookup binds the wrapper, and the instance arrives as the wrapper's first positional argument inside *args.

solid answer

~40 s

`@deco` is applied while the class body executes, to the ordinary function object `def` just produced. There is no class and no instance yet, so the decorator sees a plain function and returns a plain wrapper, and that wrapper becomes the class attribute. Binding happens later and happens to the wrapper: functions implement `__get__`, so `obj.bid` returns a bound method around the wrapper, and calling it invokes `wrapper(obj, 17)`. The instance is simply `args[0]` — nothing strips it and nothing hides it. That is why a general-purpose wrapper must be declared `def wrapper(*args, **kwargs)` and forward both through unchanged; naming the first parameter `self` silently restricts the decorator to instance methods and misbehaves on module-level functions.

code

python · 20 lines
python
import functools

def trace(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(func.__name__, "got", len(args), "positional args")
        return func(*args, **kwargs)
    return wrapper

class Bidder:
    def __init__(self, name):
        self.name = name

    @trace
    def bid(self, amount):
        return f"{self.name}:{amount}"

b = Bidder("alpha")
print(b.bid(17))
print(Bidder.bid(b, 17))

go deeper

for a junior

Recall that @deco on a method is just bid = deco(bid) run inside the class body, and that the wrapper therefore receives the instance as its first positional argument. Always write *args, **kwargs and forward both.

for a middle

Be ready to explain the two phases out loud: decoration at class-body execution on a plain function, then binding at attribute-lookup time on whatever object the class attribute now holds. Mention that functions implement __get__, which is why the wrapper binds identically.

for a senior

An interviewer expects you to show the failure mode you have actually hit: a decorator that names its first parameter self and then breaks on a module-level function or a keyword call. Show how forwarding blindly plus functools.wraps keeps one decorator usable everywhere.

for a principal

Own the convention question. Decide whether the codebase's decorators are function-shaped or method-shaped, write that down, and keep decorator layers thin — every wrapper adds a frame on a hot path and one more place where argument shape is assumed rather than declared.

**Where decoration actually happens** A decorator written inside a class body is applied *while the class body is executing*, which is normally import time. At that moment there is no class object yet, no instance, and no binding machinery in play. `def bid(self, amount): ...` produces an ordinary function object, and `@trace` immediately calls `trace(bid)`. Whatever that call returns is bound to the name `bid` in the class body's namespace, and when the class is finally created that object becomes the class attribute. So the decorator never sees a "method". It sees a function whose first parameter happens to be spelled `self`. `self` is a convention, not a keyword; the function is completely ordinary. **Where binding happens** Binding is a *lookup-time* mechanism, not a definition-time one. Function objects implement `__get__`, which makes every function a descriptor. When you write `b.bid`, attribute lookup walks the type's MRO, finds the class attribute, notices it has `__get__`, and calls it with the instance. A function's `__get__` returns a bound method — a small object that remembers the instance and prepends it to the argument list on every call. The crucial point is that this happens to *whatever object is sitting in the class dictionary*. After decoration that object is your wrapper. The wrapper is itself a plain function, so it is itself a descriptor, so it binds exactly the way the original would have. `b.bid(17)` therefore ends up calling `wrapper(b, 17)`, and the wrapper's `args` tuple is `(b, 17)`. **Consequences you are expected to state** 1. *Write `*args, **kwargs` and forward them unchanged.* This is the whole reason the idiom exists. A wrapper declared `def wrapper(self, *args, **kwargs)` works on instance methods and quietly breaks everywhere else: applied to a module-level function, the caller's first positional argument is bound to a parameter named `self`, which is merely confusing until someone calls it by keyword and gets a `TypeError`. 2. *The instance is readable if you genuinely need it.* A wrapper that wants per-instance behaviour — a per-object log prefix, a per-object counter — can read `args[0]`, but only because it has been documented as method-only. A decorator that must work on both functions and methods should not guess; it should take what it needs as a decorator-factory argument instead. 3. *Keyword calls change the shape.* `b.bid(amount=17)` gives the wrapper `args == (b,)` and `kwargs == {'amount': 17}`. Any wrapper that inspects positional arguments by index has to survive that; this is why generic wrappers forward blindly rather than reasoning about argument meaning. 4. *Decoration cost is paid once; wrapping cost is paid per call.* The decorator body runs once per decorated function at import. The wrapper body runs on every call, adding one Python-level frame. That is usually irrelevant, and occasionally is not — a wrapper on a method called in a tight loop is a real, measurable cost. 5. *The class attribute is now the wrapper, so introspection sees the wrapper.* Its `__name__` and `__doc__` are the wrapper's unless you copy them across, which is what `functools.wraps` is for. Accessing the attribute on the class rather than on an instance — `Bidder.bid` — returns the wrapper function itself, unbound, so `Bidder.bid(b, 17)` is a legitimate explicit call. **The mental model to say out loud** "`@deco` above a `def` in a class body is exactly `bid = trace(bid)` executed in the class body. Binding is not involved yet. Later, `b.bid` binds whatever object that name ended up holding — which is the wrapper — so the wrapper is the thing that receives the instance." Once that model is in place, the neighbouring surprises stop being surprising: a decorator that returns something which is *not* a function returns something which is not a descriptor, and then no instance is prepended at all; and a decorator stacked against `@staticmethod` or `@classmethod` has to respect the fact that those produce descriptor objects rather than functions. Both follow directly from "decoration happens first, binding happens to the result".

  • Why is `def wrapper(self, *args, **kwargs)` a bad signature for a general-purpose decorator?
    It hard-codes the assumption that the decorated callable is an instance method. Applied to a module-level function it binds the caller's first positional argument to a parameter named `self`, which is misleading and breaks outright if that argument is ever passed by keyword. Declaring `*args, **kwargs` and forwarding unchanged makes one decorator work on functions, methods and static methods alike.
  • How many times does the decorator body run for a method on a class with a thousand instances?
    Once. The decorator is applied while the class body executes, so it runs once per decorated function, not once per instance and not once per call. Only the wrapper body runs per call. Anything the decorator stores in a closure is therefore shared by every instance of the class.
  • What does `args` look like when the caller passes everything by keyword?
    `args` holds only the instance and `kwargs` holds the rest: `b.bid(amount=17)` gives `args == (b,)` and `kwargs == {'amount': 17}`. A wrapper that reads positional arguments by index has to tolerate that, which is another reason generic wrappers forward blindly rather than interpreting arguments.

saying these in an interview costs you the question

  • Claims binding strips self before the wrapper sees it
  • Writes wrapper(self, *args) and calls the decorator general-purpose
  • Thinks the decorator runs on every call of the method
  • Thinks the decorator runs once per instance
  • Believes self is delivered as a keyword argument
  • Cannot say that plain functions are descriptors

context