This rule raises an issue when @singledispatch is used on a method defined inside a class instead of
@singledispatchmethod, or when @singledispatchmethod is used on a standalone function instead of
@singledispatch. It also flags problematic combinations with @classmethod and @staticmethod.
The @singledispatch and @singledispatchmethod decorators from functools serve different purposes and are not
interchangeable.
@singledispatch is designed for standalone functions. It dispatches based on the type of the first positional argument. When applied
to an instance method, the first argument is self, so dispatch is performed on the type of the instance instead of on the actual data
argument. The registered type implementations (for example, for int or str) are therefore never selected based on the
data:
@singledispatchmethod is designed for instance methods and class methods. It returns a non-callable descriptor that relies on the
descriptor protocol to bind to an instance or class. When applied to a standalone function, the resulting object cannot be invoked at all, and calling
it raises a TypeError.
This rule also flags @singledispatch applied to a method decorated with @classmethod. In that case, the dispatcher
attempts to operate on a classmethod object rather than a plain function, and the call fails at runtime.
In all of these cases, no error or warning is raised at decoration time, and some failure modes only surface on the first call, making the bug silent and hard to detect by reading the code alone.
The rule’s behavior with @staticmethod depends on the decorator order:
@singledispatch placed above @staticmethod is flagged. In that order,
@singledispatch wraps the staticmethod descriptor object, which makes both registration of overloads and dispatch on
instances unreliable.@staticmethod placed above @singledispatch is not flagged, because
@singledispatch is then applied to a regular function and @staticmethod simply prevents Python from binding
self, so dispatch correctly happens on the data argument.Note that even the non-flagged order is uncommon and easy to misuse: a future refactor that swaps the order silently reintroduces the bug.
Confusing these two decorators causes the type dispatch mechanism to behave incorrectly:
@singledispatch on an instance method, calls on subclasses may resolve to different registered
implementations than calls on the base class, so the same call site can produce different results depending on the receiver’s type@singledispatch
with @classmethod or above @staticmethod typically fail only at call timeUsing @singledispatch on a class method:
from functools import singledispatch
class Processor:
@singledispatch # Noncompliant: should use @singledispatchmethod for methods
def process(self, data):
return f"Processing: {data}"
@process.register(int)
def _(self, data):
return f"Processing int: {data}"
@process.register(str)
def _(self, data):
return f"Processing str: {data}"
from functools import singledispatchmethod
class Processor:
@singledispatchmethod
def process(self, data):
return f"Processing: {data}"
@process.register(int)
def _(self, data):
return f"Processing int: {data}"
@process.register(str)
def _(self, data):
return f"Processing str: {data}"
Using @singledispatchmethod on a standalone function:
from functools import singledispatchmethod
@singledispatchmethod # Noncompliant: should use @singledispatch for standalone functions
def process(data):
return f"Processing: {data}"
@process.register(int)
def _(data):
return f"Processing int: {data}"
@process.register(str)
def _(data):
return f"Processing str: {data}"
from functools import singledispatch
@singledispatch
def process(data):
return f"Processing: {data}"
@process.register(int)
def _(data):
return f"Processing int: {data}"
@process.register(str)
def _(data):
return f"Processing str: {data}"
Using @singledispatch on a @classmethod:
from functools import singledispatch
class Processor:
@singledispatch # Noncompliant: @singledispatch wraps a classmethod descriptor
@classmethod
def process(cls, data):
return f"Processing: {data}"
@process.register(int)
@classmethod
def _(cls, data):
return f"Processing int: {data}"
from functools import singledispatchmethod
class Processor:
@singledispatchmethod
@classmethod
def process(cls, data):
return f"Processing: {data}"
@process.register
@classmethod
def _(cls, data: int):
return f"Processing int: {data}"
Use @singledispatch for standalone, module-level functions and @singledispatchmethod for instance methods and class
methods. The @singledispatch decorator dispatches on the first argument, while @singledispatchmethod skips self
or cls and dispatches on the next argument.
When combining @singledispatchmethod with @classmethod, @singledispatchmethod must remain the outermost
decorator so that the registration mechanism (process.register) is still available. @classmethod is then applied to the base
implementation and to each registered overload.
For static methods, prefer turning the function into a module-level function decorated with @singledispatch. If the dispatcher must
live on the class, place @staticmethod above @singledispatch so that @singledispatch wraps a
regular function rather than a staticmethod descriptor; the inverse order is reported by this rule. More generally, prefer
@singledispatch on a module-level function whenever the dispatcher does not need to live on a class, since stacked-decorator orders are
easy to misuse and silently regress on refactors.
functools.singledispatchfunctools.singledispatchmethod