skip to content

Why does a class-based decorator break when applied to an instance method?

level: middleimportance: should knowfreq 30%

answer

  1. Plain functions bind, arbitrary objects do not
  2. Attribute lookup asks for __get__
  3. Your decorator instance is not a descriptor
  4. Implement __get__ returning a partial

basics

~20 s

The class attribute is now an instance of your decorator class, and ordinary instances are not descriptors: they have no __get__. Attribute lookup returns the object as-is, nothing prepends the instance, and the wrapped function loses its first argument.

solid answer

~40 s

A decorator implemented as a class replaces the function with an object that defines `__init__` and `__call__`. That object is callable, so it works fine on a module-level function. Inside a class it fails, because binding is a descriptor mechanism: `obj.m` only prepends the instance when the class attribute implements `__get__`, which plain functions do and ordinary objects do not. Lookup therefore returns your decorator instance unchanged, and `obj.m(17)` calls `__call__(17)` — the wrapped function is invoked with `17` bound to `self` and its real first parameter missing, so you get a `TypeError` or, worse, silently shifted arguments. The fix is to make the decorator a descriptor: add `__get__(self, obj, objtype=None)` returning `functools.partial(self.__call__, obj)` (or `types.MethodType(self, obj)`), returning `self` when `obj` is `None` so class access still works.

code

python · 16 lines
python
class shout:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs).upper()

class Bidder:
    @shout
    def label(self, tag):
        return tag

try:
    Bidder().label("alpha")
except TypeError as exc:
    print(exc)

go deeper

for a junior

Remember the symptom and the one-line cause: a decorator written as a class turns the method into a plain object, and plain objects do not get the instance passed to them. Prefer the closure form until you need state.

for a middle

Explain the mechanism: binding happens only when the class attribute implements __get__, which functions do and ordinary instances do not. Show the fix — a __get__ returning a callable with the instance pre-bound, and self when accessed on the class.

for a senior

Demonstrate that you have debugged this: the traceback says a required argument is missing, class access still works, and the same trap catches any non-function a decorator returns. Note that the decorator object is shared across instances, so its state is class-wide.

for a principal

Own the choice between forms. Class-based decorators buy introspectable state and cost you descriptor correctness plus a per-access binding allocation; closures bind for free. Set the convention, and require any published class-based decorator to ship a __get__ and a method-level test.

**What a class-based decorator produces** ```python class shout: def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): return self.func(*args, **kwargs).upper() ``` `@shout` above a `def` means `label = shout(label)`, so the name now holds a `shout` *instance*. That instance is callable because the type defines `__call__`, and on a module-level function everything works: `shout_label("alpha")` finds `type(obj).__call__` and runs it. Inside a class body the same object becomes the class attribute — and that is where it stops behaving like a method. **Binding is descriptor work, not callable work** When you evaluate `b.label`, Python looks the name up on `type(b)` and its MRO. If the object it finds defines `__get__`, that is invoked and its return value is the attribute's value. Plain functions define `__get__`; that single fact is the entire method system. A function's `__get__` returns a bound method that remembers `b` and prepends it to every call. Your `shout` instance defines `__call__` but not `__get__`. It is a callable, not a descriptor. So lookup finds no `__get__`, falls back to returning the object itself, and `b.label("alpha")` becomes `shout_instance.__call__("alpha")`. Inside, `self.func("alpha")` calls the original `label(self, tag)` with `"alpha"` bound to `self` and `tag` missing: `TypeError: label() missing 1 required positional argument: 'tag'`. The dangerous variant is a wrapped function whose arity happens to absorb the shift — then there is no error at all, just an instance sitting where a real argument belonged. This is not special to hand-written decorator classes. Anything non-function returned by a decorator has the same problem. `functools.partial` objects are not descriptors either, which is precisely why the standard library ships `functools.partialmethod` for the in-class case. **The fix: implement `__get__`** ```python def __get__(self, obj, objtype=None): if obj is None: return self return functools.partial(self.__call__, obj) ``` Three things are going on. `obj is None` is the class-access path (`Bidder.label`); returning `self` keeps the decorator object reachable, which is what lets you read whatever it recorded. Otherwise you return a callable that already has the instance baked in as the first positional argument, reproducing exactly what a bound method does. `types.MethodType(self, obj)` is the alternative and is arguably more faithful — it produces a real bound-method object whose call is `self(obj, ...)` — while `functools.partial` is cheaper and prints less usefully in a traceback. Either is acceptable in an interview as long as you can say what it reconstructs. **The consequences you should raise unprompted** *One decorator object per decorated function, not per instance.* The class body runs once, so a single `shout` instance is shared by every instance of the owning class. Any counter or cache it keeps is therefore class-wide, not per-object — a behaviour difference from the closure-based form that people routinely trip over. *Caching the bound callable is optional and has a cost.* Because a `__get__`-only descriptor is a *non-data* descriptor, the instance `__dict__` wins on lookup. You can therefore store the bound wrapper into `obj.__dict__` the first time and skip `__get__` afterwards, avoiding a fresh `functools.partial` per access. That is a real optimisation and a real trap: it fails on a class with `__slots__`, and it makes the wrapper's lifetime the instance's lifetime. *The failure only appears on instance access.* `Bidder.label` never had a bug, which is why this survives casual testing and shows up the first time somebody uses the decorator on a method rather than a function. **When to choose which form** If the decorator carries no state, the closure form — a `def` returning a nested function — is smaller, binds for free because it returns a function, and needs no descriptor knowledge. Reach for the class form when you genuinely want a named object with attributes and methods hanging off it, and then accept that you owe it a `__get__`. In an interview, the sentence that lands is: **"functions are descriptors; my object is not, so I have to be one."**

  • Why does `__get__` return `self` when `obj` is `None`?
    That is the class-access path: `Bidder.bid` calls `__get__(None, Bidder)`. Returning the decorator object itself keeps whatever it recorded reachable — counters, caches, the wrapped function — and mirrors how a plain function returns itself on class access instead of a bound method.
  • Is the decorator object created once per class or once per instance?
    Once per decorated function, when the class body executes. Every instance of the owning class shares that single object, so any state it keeps is class-wide rather than per-instance. If you need per-instance state, key it by the instance inside `__call__` rather than storing it as a plain attribute.
  • What is the difference between returning `functools.partial(self.__call__, obj)` and `types.MethodType(self, obj)`?
    Both reproduce binding. `types.MethodType` builds a genuine bound-method object around the decorator instance, so it reads and reprs like a method; `functools.partial` is a lighter object with the instance pre-bound as the first positional argument. Behaviourally equivalent for calling; the method object introspects better.

saying these in an interview costs you the question

  • Thinks defining __call__ is enough to make an object a method
  • Blames the class body or the @ syntax rather than binding
  • Claims Python passes self to any callable class attribute
  • Suggests adding a self parameter to __call__ as the fix
  • Cannot name __get__ as the missing piece
  • Assumes the decorator object is created per instance

context