A class-based decorator that works perfectly on standalone functions will produce a TypeError: missing 1 required positional argument: 'self' the moment you apply it to an instance method. This is not a bug in your decorator. It is a consequence of how Python turns plain functions into bound methods through the descriptor protocol -- a mechanism that class-based decorators bypass unless you explicitly implement it. These Python tutorials cover this problem from the ground up, show why function-based decorators do not have this issue, and walk through every approach for fixing it in class-based decorators.
To understand the problem, you first need to understand what Python does when you access a method on an instance. When you write obj.method(), Python does not simply look up a function and call it. It invokes the descriptor protocol, which transforms the function into a bound method with self pre-filled. This transformation is the critical step that class-based decorators inadvertently disable.
__get__
__init__ takes args, returns callable decorator
types.MethodType
__reduce__ implemented
Why Function-Based Decorators Handle self Automatically
A function-based decorator returns a plain function. Python functions implement __get__ as part of their type, which means they participate in the descriptor protocol. When Python finds a function in a class's attribute dictionary and you access it through an instance, function.__get__ fires and produces a bound method with the instance pre-filled as the first argument.
pythonimport functools
def log_calls(func):
"""Function-based decorator -- works on methods automatically."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Greeter:
def __init__(self, name):
self.name = name
@log_calls
def hello(self):
return f"Hello from {self.name}"
g = Greeter("Alice")
print(g.hello())
# Calling hello
# Hello from Alice
This works because log_calls returns wrapper, which is a regular function. When Python looks up g.hello, it finds wrapper in Greeter.__dict__, calls wrapper.__get__(g, Greeter), and produces a bound method. The bound method, when called, passes g as the first argument to wrapper. Inside wrapper, *args captures (g,), and func(*args, **kwargs) calls the original hello with self=g.
The key insight: function-based decorators work on instance methods for free because the wrapper is a function, and functions are descriptors. More precisely, according to Raymond Hettinger's official Python Descriptor Guide, functions are non-data descriptors that return bound methods during dotted lookup from an instance.
A non-data descriptor defines __get__ but not __set__ or __delete__. This distinction matters: because functions lack __set__, an instance dictionary entry with the same name as a method takes precedence over the method during attribute lookup. That is how instances can shadow methods — and it is the same mechanism a class-based decorator without __get__ inadvertently exploits in reverse, bypassing binding entirely.
self on instance methods automatically, without any extra code?Why Class-Based Decorators Break on Instance Methods
A class-based decorator replaces the function with an instance of the decorator class. That instance is not a function. Unless the decorator class defines __get__, it is not a descriptor. When Python looks up the attribute on an instance, it finds the decorator instance but has no mechanism to bind self. Specifically, it is object.__getattribute__ that checks whether an attribute found in the class dictionary is a descriptor and, if so, calls its __get__. When no __get__ is present, object.__getattribute__ returns the object as-is with no transformation.
pythonimport functools
class LogCalls:
"""Class-based decorator -- BROKEN on instance methods."""
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
return self.func(*args, **kwargs)
class Greeter:
def __init__(self, name):
self.name = name
@LogCalls
def hello(self):
return f"Hello from {self.name}"
g = Greeter("Alice")
# Works fine on standalone functions:
@LogCalls
def standalone():
return "standalone works"
print(standalone()) # Calling standalone \n standalone works
# Fails on instance methods:
print(g.hello())
# Calling hello
# TypeError: Greeter.hello() missing 1 required positional argument: 'self'
Here is exactly what happens step by step when g.hello() is called:
- 1Python looks up
"hello"ing.__dict__. It is not there. - 2Python looks up
"hello"inGreeter.__dict__. It finds aLogCallsinstance. - 3Python checks whether the
LogCallsinstance is a descriptor (has__get__). It does not. - 4Python returns the
LogCallsinstance directly. No binding occurs. - 5The caller invokes it with
().LogCalls.__call__fires withargs=()andkwargs={}. - 6
self.func(*args, **kwargs)calls the originalhello()with no arguments. - !
helloexpectsselfas its first argument. It gets nothing.TypeError.
The self inside LogCalls.__call__ refers to the LogCalls instance, not the Greeter instance. The Greeter instance is never passed anywhere.
This bug is subtle because the decorator works correctly on standalone functions. The TypeError only appears when the decorator is used on instance methods. If your tests only cover standalone function usage, the bug goes undetected until the decorator is applied inside a class.
__get__ is not defined on the decorator class. When you call g.hello(), at exactly which step does the failure occur?The Fix: Implementing __get__
The fix is to make the decorator class a descriptor by implementing __get__. When Python accesses the decorated method on an instance, __get__ intercepts the lookup and returns a callable with the instance already bound.
pythonimport functools
class LogCalls:
"""Class-based decorator -- works on both functions and methods."""
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
# Accessed through the class, not an instance
return self
# Accessed through an instance: bind obj as the first argument
return functools.partial(self.__call__, obj)
class Greeter:
def __init__(self, name):
self.name = name
@LogCalls
def hello(self):
return f"Hello from {self.name}"
g = Greeter("Alice")
print(g.hello())
# Calling hello
# Hello from Alice
# Still works on standalone functions:
@LogCalls
def standalone():
return "standalone works"
print(standalone())
# Calling standalone
# standalone works
Now when Python looks up g.hello, it finds the LogCalls instance in Greeter.__dict__, sees that it has __get__, and calls LogCalls.__get__(g, Greeter). Because obj is not None (it is the Greeter instance), __get__ returns functools.partial(self.__call__, g). When this partial is called with (), it invokes self.__call__(g), which calls self.func(g), which calls hello(g) -- exactly what was needed.
By implementing only __get__ on the decorator class, you create a non-data descriptor — the same classification as a plain Python function. According to the Python data model documentation, Python methods (including those decorated with @staticmethod and @classmethod) are implemented as non-data descriptors.
A class-based decorator with only __get__ participates in the same lookup chain as ordinary methods. If you also defined __set__, it would become a data descriptor, which takes precedence over instance __dict__ entries — making it impossible for an instance to shadow the decorated attribute. For a decorator, this is almost never what you want, so do not add __set__ unless you have an explicit reason.
__get__ to return a bound callable. A colleague says to always use types.MethodType because it is "more correct." When is functools.partial actually the better choice?Alternative: Using types.MethodType
Instead of functools.partial, you can use types.MethodType to create a proper bound method. This is closer to what Python does internally for regular functions:
pythonimport functools
import types
class LogCalls:
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
# Create a bound method: obj is pre-bound as first arg to __call__
return types.MethodType(self, obj)
When types.MethodType(self, obj) is called, the resulting bound method, when invoked, calls self.__call__(obj, ...). This produces the same behavior as the functools.partial approach but creates an object that inspect recognizes as a bound method.
functools.partial
Simpler. Creates a partial object with obj pre-filled. Works everywhere. Does not pass inspect.ismethod() — the result is a functools.partial, not a bound method. Best choice for most decorators.
types.MethodType
Creates a true bound method object. Passes inspect.ismethod() checks. Necessary when downstream code (frameworks, serializers, debuggers) inspects whether the callable is a method. Marginally more overhead.
Building a Universal Class-Based Decorator
A robust class-based decorator should work on standalone functions, instance methods, and methods called through the class. Here is a complete template that handles all three:
pythonimport functools
import random
import time
class Timer:
"""Decorator that measures execution time.
Works on standalone functions, instance methods, and classmethods.
"""
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
self.total_time = 0.0
self.call_count = 0
def __call__(self, *args, **kwargs):
start = time.perf_counter()
result = self.func(*args, **kwargs)
elapsed = time.perf_counter() - start
self.total_time += elapsed
self.call_count += 1
print(f"{self.func.__name__} took {elapsed:.6f}s "
f"(total: {self.total_time:.6f}s over {self.call_count} calls)")
return result
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self.__call__, obj)
# Works on standalone functions
@Timer
def compute(n):
return sum(range(n))
compute(1_000_000)
compute(2_000_000)
# compute took 0.021234s (total: 0.021234s over 1 calls)
# compute took 0.042891s (total: 0.064125s over 2 calls)
# Works on instance methods
class DataProcessor:
def __init__(self, data):
self.data = data
@Timer
def process(self):
return sorted(self.data)
@Timer
def summarize(self):
return {
"count": len(self.data),
"sum": sum(self.data),
"mean": sum(self.data) / len(self.data),
}
dp = DataProcessor([random.randint(0, 10000) for _ in range(100_000)])
dp.process()
dp.summarize()
# process took 0.012345s (total: 0.012345s over 1 calls)
# summarize took 0.003456s (total: 0.003456s over 1 calls)
This Timer class maintains state (total_time, call_count) across calls, which is the primary reason to use a class-based decorator instead of a function-based one. The __get__ method ensures it works on instance methods by binding the instance through functools.partial.
Handling @classmethod and @staticmethod
When your class-based decorator needs to coexist with @classmethod or @staticmethod, decorator stacking order matters. The outermost decorator is applied last, so it wraps whatever the inner decorator produced.
pythonclass Example:
# Correct: @Timer wraps the raw function, @classmethod wraps the Timer
@classmethod
@Timer
def class_factory(cls, value):
return cls(value)
# Correct: @Timer wraps the raw function, @staticmethod wraps the Timer
@staticmethod
@Timer
def utility(x, y):
return x + y
def __init__(self, value):
self.value = value
# Both work correctly:
obj = Example.class_factory(42)
print(obj.value) # 42
result = Example.utility(3, 4)
print(result) # 7
The stacking reads bottom-up: @Timer is applied first (wrapping the raw function), then @classmethod or @staticmethod wraps the Timer instance. Because @classmethod and @staticmethod implement their own __get__, they handle the binding correctly even though the wrapped object is a Timer instance rather than a plain function. If you reverse the order, placing @Timer outside @classmethod, the Timer wraps a classmethod descriptor object, which is not callable, and the decorator breaks.
@Timer decorator to a classmethod. Which of the following is correct?Bonus: Using __set_name__ for Richer Introspection
Python 3.6 introduced __set_name__ (via PEP 487), which is called on a descriptor when the class it belongs to is created. It receives the owner class and the name the descriptor was assigned to. For a class-based decorator used as a method decorator, this means you can capture the attribute name at class creation time rather than relying solely on func.__name__.
pythonimport functools
class LogCalls:
def __init__(self, func):
self.func = func
self.owner_class = None # filled by __set_name__
self.attr_name = None # filled by __set_name__
functools.update_wrapper(self, func)
def __set_name__(self, owner, name):
# Called once when the class body is executed
self.owner_class = owner
self.attr_name = name
def __call__(self, *args, **kwargs):
owner = self.owner_class.__name__ if self.owner_class else "?"
name = self.attr_name or self.func.__name__
print(f"Calling {owner}.{name}")
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self.__call__, obj)
class Service:
@LogCalls
def process(self):
return "processed"
s = Service()
s.process()
# Calling Service.process
__set_name__ is called once, at class definition time, not per invocation. This makes it useful for building decorators that integrate with class hierarchies, produce better error messages, or generate derived attribute names without any runtime overhead.
Real-World Example: Call Counter With Statistics
__get__ binding for instance methodsstatistics modulereport_all() classmethod for monitoringHere is a production-ready class-based decorator that tracks call frequency and timing statistics, works on all method types, and exposes its data for monitoring:
pythonimport functools
import time
import statistics as stats
class Monitor:
"""Track call count, timing, and error rate for a function or method."""
_registry = {}
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
self.timings = []
self.error_count = 0
Monitor._registry[func.__qualname__] = self
def __call__(self, *args, **kwargs):
start = time.perf_counter()
try:
result = self.func(*args, **kwargs)
self.timings.append(time.perf_counter() - start)
return result
except Exception:
self.error_count += 1
self.timings.append(time.perf_counter() - start)
raise
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self.__call__, obj)
@property
def call_count(self):
return len(self.timings)
def report(self):
if not self.timings:
return f"{self.func.__qualname__}: no calls"
lines = [
f"{self.func.__qualname__} over {self.call_count} calls:",
f" mean = {stats.mean(self.timings):.6f}s",
f" median = {stats.median(self.timings):.6f}s",
f" min = {min(self.timings):.6f}s",
f" max = {max(self.timings):.6f}s",
f" errors = {self.error_count}",
]
if len(self.timings) > 1:
lines.append(f" stdev = {stats.stdev(self.timings):.6f}s")
return "\n".join(lines)
@classmethod
def report_all(cls):
return "\n\n".join(m.report() for m in cls._registry.values())
class OrderService:
def __init__(self, db):
self.db = db
@Monitor
def create_order(self, items):
time.sleep(0.01) # simulated DB write
return {"id": 1, "items": items}
@Monitor
def get_order(self, order_id):
time.sleep(0.005) # simulated DB read
return {"id": order_id}
service = OrderService("postgres://localhost/shop")
for _ in range(10):
service.create_order(["widget", "gadget"])
for i in range(20):
service.get_order(i)
print(Monitor.report_all())
The Monitor class maintains a global registry of all monitored functions and methods. Each instance tracks its own timings and error count. The __get__ method ensures it works on instance methods. The report_all classmethod provides a single entry point for dumping all monitoring data. This kind of persistent state across invocations is the scenario where class-based decorators offer a clear advantage over function-based ones.
The Monitor and Timer examples above use shared mutable state — self.timings, self.total_time, Monitor._registry. If two threads call the same decorated method simultaneously, they will race on list append and float addition. For thread-safe production use, protect writes with a threading.Lock: acquire it around self.timings.append(...) in __call__. Alternatively, use threading.local() if you want per-thread statistics rather than shared totals.
Parameterized Class-Based Decorators
Every example so far uses a decorator with no arguments: @LogCalls. A parameterized decorator — one written as @LogCalls(prefix="[SVC]") — requires a different structure. The decorator class itself can no longer receive the function in __init__, because __init__ receives the arguments instead. There are two clean patterns.
Pattern 1 — the class is the factory, returns a different callable:
pythonimport functools
class LogCalls:
"""Parameterized class-based decorator.
Usage:
@LogCalls(prefix="[SVC]")
def my_method(self): ...
"""
def __init__(self, prefix=""):
# __init__ receives decorator arguments, NOT the function
self.prefix = prefix
def __call__(self, func):
# __call__ receives the function and returns the wrapper.
# Returning a plain function means Python's built-in function.__get__
# handles instance binding automatically — no custom __get__ needed.
prefix = self.prefix
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"{prefix} Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Service:
@LogCalls(prefix="[SVC]")
def process(self, data):
return f"processed: {data}"
s = Service()
print(s.process("hello"))
# [SVC] Calling process
# processed: hello
Pattern 2 — keep the class as the wrapper, use a classmethod factory:
pythonimport functools
class LogCalls:
def __init__(self, func, prefix=""):
self.func = func
self.prefix = prefix
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f"{self.prefix} Calling {self.func.__name__}")
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self.__call__, obj)
@classmethod
def with_args(cls, prefix=""):
"""Factory for parameterized usage: @LogCalls.with_args(prefix='[X]')"""
def decorator(func):
return cls(func, prefix=prefix)
return decorator
# No-argument usage (unchanged):
@LogCalls
def standalone():
return "ok"
# Parameterized usage:
class Service:
@LogCalls.with_args(prefix="[SVC]")
def process(self, data):
return f"processed: {data}"
s = Service()
s.process("hello")
# [SVC] Calling process
Pattern 2 is preferred when the decorator's core logic should live in __call__ and the class-based structure already exists. Pattern 1 is simpler when the parameterized version is the primary API and no-argument usage is secondary.
@LogCalls(prefix="[API]") above a method. Which method on the LogCalls class receives the decorated function as its argument?Introspection, __repr__, and functools.update_wrapper
There is a subtle difference between functools.update_wrapper(self, func) (used in __init__) and @functools.wraps(func) (used inside function-based decorators). They do the same thing — copy __module__, __name__, __qualname__, __annotations__, __doc__, and update __dict__ — but @wraps is syntactic sugar that calls update_wrapper on the function being defined. Since a class-based decorator does not define a wrapper function, update_wrapper(self, func) is called directly in __init__.
What update_wrapper does not fix is __repr__. After decoration, the decorator instance's repr will read something like <__main__.LogCalls object at 0x...> rather than showing the function name. If your tooling, logging, or test output depends on reading repr(decorated_method), implement __repr__ explicitly:
pythonclass LogCalls:
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __repr__(self):
return f""
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self.__call__, obj)
class Greeter:
@LogCalls
def hello(self): ...
print(repr(Greeter.hello))
#
Also note that update_wrapper sets a __wrapped__ attribute on the decorator instance pointing to the original function. This allows tools like inspect.unwrap() to peel back decoration layers and reach the original function — useful in testing and introspection scenarios.
Performance Considerations
Every time a decorated instance method is accessed via dotted lookup (e.g. obj.method), Python calls __get__, which creates a new functools.partial or types.MethodType object. This allocation happens on every access, not just on calls. In hot paths — tight loops calling obj.method() thousands of times per second — this overhead is measurable. If profiling shows the decorator as a bottleneck, cache the bound callable in the instance dictionary instead of recreating it each time:
pythonimport functools
class LogCalls:
"""Caching __get__ — creates the partial once per instance, not per access."""
def __init__(self, func):
self.func = func
self.attr_name = None
functools.update_wrapper(self, func)
def __set_name__(self, owner, name):
self.attr_name = name
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
# Cache the bound partial in the instance dict under the method name.
# Next access finds it in obj.__dict__ directly (instance dict wins
# over non-data descriptors), so __get__ is never called again.
bound = functools.partial(self.__call__, obj)
functools.update_wrapper(bound, self.func)
if self.attr_name:
obj.__dict__[self.attr_name] = bound
return bound
class Greeter:
@LogCalls
def hello(self):
return "hello"
g = Greeter()
g.hello() # __get__ called, partial cached in g.__dict__['hello']
g.hello() # reads directly from g.__dict__ — __get__ NOT called again
# Calling hello
# Calling hello
The caching pattern works because a class-based decorator with only __get__ is a non-data descriptor. Non-data descriptors yield to instance dictionary entries — so once obj.__dict__['hello'] is populated, subsequent lookups return the cached partial without invoking __get__ at all. Do not use this pattern if the decorator needs to regenerate the bound callable on each access (e.g. if it reads dynamic per-instance state).
References and Sources
The technical claims in this article are grounded in the following official Python documentation sources. All code behavior was verified against CPython.
- Python Descriptor Guide (Raymond Hettinger, Python Software Foundation) — The authoritative reference on the descriptor protocol, how functions become bound methods, and the pure-Python equivalents of
classmethod,staticmethod,property, andfunctools.cached_property. Available at docs.python.org/3/howto/descriptor.html. - Python Data Model (Python Software Foundation) — Documents the distinction between data descriptors (
__get__+__set__) and non-data descriptors (__get__only), and establishes that Python methods — including those decorated with@staticmethodand@classmethod— are classified as non-data descriptors. Available at docs.python.org/3/reference/datamodel.html. - functools — Higher-order functions (Python Software Foundation) — Documents
functools.update_wrapper,functools.wraps, andfunctools.partial, including defaultWRAPPER_ASSIGNMENTS(__module__,__name__,__qualname__,__annotations__,__type_params__,__doc__) and the__wrapped__attribute thatupdate_wrapperautomatically adds. Available at docs.python.org/3/library/functools.html. - PEP 487 — Simpler customisation of class creation (Martin Teichmann, 2016) — Introduced
__set_name__in Python 3.6, which allows descriptors to receive the attribute name and owner class automatically at class creation time. Available at peps.python.org/pep-0487. - types — Dynamic type creation and names for built-in types (Python Software Foundation) — Documents
types.MethodType, which creates a bound method object by associating a callable with an instance. Available at docs.python.org/3/library/types.html.
Frequently Asked Questions
- Why does a class-based decorator cause a TypeError on instance methods?
- A class-based decorator replaces the method with an instance of the decorator class. Because that instance does not define
__get__, it is not a descriptor. When Python looks up the attribute on an instance, it returns the decorator object directly with no binding, so the original method is called without the instance as the first argument and raisesTypeError: missing 1 required positional argument: 'self'. - Why do function-based decorators handle self automatically?
- Function-based decorators return plain functions. Python functions implement
__get__as non-data descriptors. When a function is accessed through a class instance,function.__get__is called automatically and returns a bound method with the instance pre-filled as the first argument. This binding step is what passesselfto the wrapped function. - How do you fix a class-based decorator so it works on instance methods?
- Implement
__get__on the decorator class. Whenobjis notNone(accessed through an instance), returnfunctools.partial(self.__call__, obj)ortypes.MethodType(self, obj)to bind the instance as the first argument. WhenobjisNone(accessed through the class), returnselfunchanged. - What is the difference between functools.partial and types.MethodType in __get__?
- Both produce a callable that binds the instance as the first argument.
functools.partialcreates a partial object, which is simpler.types.MethodTypecreates a proper bound method object that passesinspect.ismethod()checks. Usefunctools.partialfor simplicity; usetypes.MethodTypewhen downstream code expects a bound method object or usesinspectto verify method type. - What is the correct stacking order when combining a class-based decorator with @classmethod or @staticmethod?
- Place your class-based decorator below (inside)
@classmethodor@staticmethodin the source. Decorators are applied bottom-up, so your decorator wraps the raw function first, then@classmethodor@staticmethodwraps the decorator instance. Reversing the order causes the decorator to receive an unresolved descriptor object rather than the raw function. - What is a non-data descriptor in Python?
- A non-data descriptor is an object that defines
__get__but not__set__or__delete__. Python functions are non-data descriptors. Because they do not define__set__, instance dictionary entries take precedence over them during attribute lookup, which means instances can shadow methods by setting an attribute of the same name. - Are class-based decorators thread-safe?
- Not by default. Class-based decorators that store mutable state — call counts, timing lists, registries — will have race conditions when decorated methods are called from multiple threads simultaneously. Protect writes with
threading.Lockinside__call__, or usethreading.local()if you want per-thread statistics rather than shared totals.
How to Fix a Class-Based Decorator on Instance Methods
- 1Identify the problem. Apply the decorator to an instance method and call it through an instance. If you see
TypeError: missing 1 required positional argument: 'self', the decorator class does not implement__get__. - 2Add
__get__to the decorator class. The method signature isdef __get__(self, obj, objtype=None):. This makes the decorator a non-data descriptor, participating in Python's attribute lookup chain. - 3Handle the
obj is Nonecase. When the decorated method is accessed through the class rather than an instance,objisNone. Returnselfunchanged so the decorator still works in unbound contexts. - 4Bind the instance when
objis notNone. Returnfunctools.partial(self.__call__, obj)to pre-fill the instance as the first argument. Alternatively, returntypes.MethodType(self, obj)if downstream code checksinspect.ismethod(). - 5Verify on standalone functions. Call the decorated standalone function and confirm it still works correctly. The
obj is Nonebranch is not triggered for standalone functions because they are not looked up through an instance. - 6Test with
@classmethodand@staticmethodif needed. Place your decorator below (inside) the built-in decorator in the source. Confirm the stacking order is correct by calling both the class method and the static method after applying your decorator.
Key Takeaways
- Function-based decorators handle
selfautomatically because they return functions, and functions are descriptors. Python's defaultfunction.__get__binds the instance asselfduring attribute lookup. No extra work is needed. - Class-based decorators replace the method with an object that is not a function and not a descriptor. Without
__get__, Python returns the decorator instance directly, and noselfbinding occurs. The original method never receives the instance. - Implementing
__get__on the decorator class restores method binding. Whenobjis notNone, returnfunctools.partial(self.__call__, obj)ortypes.MethodType(self, obj)to pre-fill the instance as the first argument. - When
__get__receivesobj=None, the access is through the class, not an instance. Returnselfunchanged in that case, which preserves the ability to call the method on the class directly (for unbound usage). - When stacking with
@classmethodor@staticmethod, place your decorator inside (below) the built-in decorator. The built-in descriptor should be the outermost wrapper so its__get__handles the binding protocol for the specific method type. - Adding only
__get__makes the decorator a non-data descriptor, matching ordinary method behavior. Non-data descriptors yield to instance dictionary entries during lookup, so instances can still shadow decorated methods. Adding__set__would promote the decorator to a data descriptor that takes priority over instance__dict__— almost never the correct behavior for a decorator. - Python 3.6+ class-based decorators can implement
__set_name__to capture the owner class and attribute name at class creation time. This is called once when the class body executes, not per invocation, and enables richer introspection and error messages with no runtime cost. - Parameterized class-based decorators require a structural shift. When using
@LogCalls(prefix="x"),__init__receives the arguments, not the function. The function arrives via__call__or a factory classmethod. Keep the pattern explicit so readers of the code can distinguish argument-receiving from function-receiving phases. functools.update_wrappercopies metadata but does not fix__repr__. Implement__repr__on the decorator class if tooling, logging, or tests readrepr()of decorated methods. The__wrapped__attribute added byupdate_wrapperletsinspect.unwrap()reach the original function through any number of decoration layers.- Class-based decorators with
__get__create a new partial or bound method on every attribute access, not just on calls. In hot paths, store the bound callable inobj.__dict__inside__get__so subsequent lookups bypass the descriptor entirely. This works because non-data descriptors yield to instance dictionary entries. - Class-level mutable state requires explicit thread synchronization. Lists, counters, and registries shared across method calls are not thread-safe by default. Use
threading.Lockaround writes, orthreading.local()for per-thread isolation.
The descriptor protocol is the mechanism Python uses to turn attributes into bound methods, properties, class methods, and static methods. Understanding that a class-based decorator is just an attribute in a class's dictionary — and that it needs __get__ to participate in binding — is what separates class-based decorators that work everywhere from those that fail silently on instance methods. The distinction between data and non-data descriptors, the availability of __set_name__ in Python 3.6+, and the precise role of object.__getattribute__ in triggering descriptor lookup are the surrounding details that give a complete picture of how binding actually works at the language level.