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.

Why is this an issue?

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.

Exceptions

The rule’s behavior with @staticmethod depends on the decorator order:

Note that even the non-flagged order is uncommon and easy to misuse: a future refactor that swaps the order silently reintroduces the bug.

What is the potential impact?

Confusing these two decorators causes the type dispatch mechanism to behave incorrectly:

How to fix it

Code examples

Noncompliant code example

Using @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}"

Compliant solution

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}"

Noncompliant code example

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}"

Compliant solution

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}"

Noncompliant code example

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}"

Compliant solution

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}"

How does this work?

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.

Resources

Documentation