skip to content

Why must `@staticmethod` sit outermost when stacked with your own decorator?

level: middleimportance: should knowfreq 45%

answer

  1. Decorators apply bottom-up
  2. staticmethod returns a descriptor, not a function
  3. Descriptor-producing decorators go outermost
  4. 3.10 made staticmethod objects callable

basics

~20 s

Decorators apply bottom-up, and staticmethod returns a descriptor object rather than a function. Put it outermost so it wraps your wrapper; put it underneath and your wrapper becomes an ordinary class attribute that binds as an instance method.

solid answer

~40 s

Stacked decorators apply bottom-up: the one nearest the `def` runs first. `staticmethod` and `classmethod` do not return functions — they return descriptor objects whose `__get__` decides what, if anything, gets prepended at call time. So the correct order is `@staticmethod` **above** `@trace`: your wrapper is built first, then `staticmethod` wraps the wrapper and the class attribute is a real static method. Inverted, `trace` receives a `staticmethod` object and returns a plain function, so the class attribute is an ordinary function that binds normally — instance access prepends the instance and the underlying function gets an argument it never asked for. Since Python 3.10 `staticmethod` objects are directly callable, so this fails at the arity check instead of with the older "object is not callable". `classmethod` objects are still not callable at all.

code

python · 25 lines
python
import functools

def trace(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

class Auction:
    @staticmethod
    @trace
    def floor_price(cents):
        return cents

    @trace
    @staticmethod
    def broken_floor(cents):
        return cents

print(Auction.floor_price(17), Auction().floor_price(17))
print(Auction.broken_floor(17))
try:
    Auction().broken_floor(17)
except TypeError as exc:
    print("instance access:", exc)

go deeper

for a junior

Remember the shape: @staticmethod and @classmethod always go on top of your own decorator. Recall that stacked decorators apply from the bottom up, so the one nearest the def runs first.

for a middle

Explain the mechanics: those two builtins return descriptor objects, not functions, so a plain wrapper placed above them produces an ordinary class attribute that binds and receives the instance. Mention that class access still works, which hides the bug.

for a senior

Show the diagnosis. Say what the failure looks like in a traceback, why 3.10 turned the old "not callable" crash into an arity error, and how you make a shared decorator order-proof by detecting the descriptor type and rebuilding it around the wrapper.

for a principal

Own the generalisation and the guardrail: descriptor-producing decorators must be outermost, and any decorator published for other teams should either enforce that or handle it. Decide whether your codebase pays three lines per decorator or documents an ordering rule nobody reads.

**The rule, and why it is a rule** Stacked decorators apply from the bottom up, so ```python @a @b def f(): ... ``` is exactly `f = a(b(f))`. `b` sees the raw function; `a` sees whatever `b` returned. Everything about ordering against `@staticmethod` and `@classmethod` follows from one extra fact: **those two do not return functions.** `staticmethod(func)` returns a `staticmethod` object and `classmethod(func)` returns a `classmethod` object. Both are descriptors — objects implementing `__get__` — and their whole job is to change what happens at attribute-lookup time. A `staticmethod`'s `__get__` returns the underlying function untouched, so nothing is prepended. A `classmethod`'s `__get__` returns a method bound to the *class*, so the class is prepended. That makes them **outermost-only** decorators. They must be the last thing applied, because the object they produce is not a function and is not something an ordinary wrapper knows how to handle. **The correct order** ```python @staticmethod @trace def floor_price(cents): ... ``` Bottom-up: `trace(floor_price)` builds a plain wrapper function; `staticmethod(wrapper)` turns it into a static-method descriptor; the class attribute is that descriptor. Now `Auction.floor_price(17)` and `Auction().floor_price(17)` both call the wrapper with exactly `(17,)`, and the wrapper forwards to the original. Everything behaves. **The inverted order, and exactly what breaks** ```python @trace @staticmethod def floor_price(cents): ... ``` Bottom-up: `staticmethod(floor_price)` produces the descriptor, then `trace` receives *that object* and returns a plain wrapper function. Two things are now wrong. First, the class attribute is an ordinary function, so the static-ness is gone. Instance access binds it like any method, and `Auction().floor_price(17)` calls `wrapper(instance, 17)`. The wrapper forwards both to the `staticmethod` object, which passes them to `floor_price`, which declares one parameter. You get a `TypeError` about too many positional arguments — and when the arities happen to line up, you get no error at all and a silently wrong value, with the instance sitting where the first real argument should be. Class access, `Auction.floor_price(17)`, still works, which is why this bug survives a quick smoke test. Second, the wrapper is now closed over a descriptor object rather than a function. Calling it works on modern Python only because **Python 3.10 made `staticmethod` objects directly callable**; before that the same code raised `TypeError: 'staticmethod' object is not callable` the moment it ran. That change also gave `staticmethod` and `classmethod` objects the method attributes (`__name__`, `__doc__`, `__qualname__`, `__module__`) plus `__wrapped__`, so `functools.wraps` now copies sensible metadata instead of falling back to the wrapper's own. Net effect: the newer interpreter turned a loud crash into a quieter misbehaviour. `classmethod` has no equivalent softening: `classmethod` objects are still not callable, so an inverted stack there raises `TypeError` on the first call. Separately, chaining `classmethod` on top of another descriptor — the `@classmethod @property` trick added in 3.9 — was deprecated in 3.11 and **removed in 3.13**, so it is not an option on 3.14. **Making a decorator order-proof** If a decorator is going to be applied by other people, it can simply detect the case and rebuild the descriptor around the wrapper: ```python if isinstance(func, (staticmethod, classmethod)): return type(func)(trace(func.__func__)) ``` `__func__` is the underlying function held by either object, so this unwraps, decorates, and re-wraps in the same kind. It costs three lines and removes an entire class of report. **The generalisation worth stating** The rule is not really about `staticmethod`. It is: *a decorator that returns a descriptor must be outermost, and a decorator that returns a plain function must sit below it.* `property` behaves the same way — decorate the getter, then make it a property, never the reverse. Once you frame it as "descriptor-producing decorators go on top", you never have to memorise the two special cases.

  • What happens if your wrapper is stacked above `@classmethod` instead of `@staticmethod`?
    It fails harder. `classmethod` objects are not callable on any current version, so the wrapper raises `TypeError` the first time it forwards the call. There is no 3.10-style softening for `classmethod`, which at least makes the mistake obvious immediately instead of surfacing as a wrong argument.
  • Why does the inverted stack still work when the method is called on the class rather than an instance?
    Class access does not bind a plain function — `Auction.broken_floor` returns the wrapper itself, so it is called with only the arguments you passed. Instance access is what prepends the instance. A test suite that only ever calls the helper through the class will never see the bug.
  • How would you write one decorator that survives either ordering?
    Check for the descriptor types at decoration time and rebuild: if the incoming object is a `staticmethod` or `classmethod`, unwrap it with `__func__`, decorate the underlying function, and wrap the result back in the same type. Three lines, and the decorator becomes order-independent.

saying these in an interview costs you the question

  • Says decorators apply top-down
  • Thinks staticmethod returns a plain function
  • Claims either order works because Python normalizes it
  • Tests only class access and declares the inverted stack fine
  • Believes functools.wraps repairs a mis-ordered stack
  • Suggests chaining classmethod over property on 3.14

context