functools.singledispatchmethod Register on a Subclass Leaks Into the Base Class (tested)

functools.singledispatchmethod looks like ordinary method inheritance: a subclass gets its own copy of the dispatch table, so registering a new type on the subclass should only affect that subclass. That assumption is wrong whenever the subclass doesn't redeclare the singledispatchmethod itself. Because the subclass inherits the exact same descriptor object as the base class, calling .register through the subclass mutates one shared dispatch table -- and the base class, and every other subclass that also just inherits it, silently pick up the new overload too.

1. Registering through a subclass changes the base class's dispatch

from functools import singledispatchmethod

class Base:
    @singledispatchmethod
    def handle(self, arg):
        return f"base default {arg!r}"

    @handle.register
    def _(self, arg: int):
        return f"base int {arg}"

class Sub(Base):
    pass  # does not redefine handle at all

@Sub.handle.register
def _(self, arg: str):
    return f"sub str {arg}"

print(Base().handle("x"))
print(Sub().handle("x"))
sub str x
sub str x

Measured: Sub never redeclared handle, so Sub.handle is literally the same object as Base.handle -- the same underlying dispatcher and dispatch registry. Calling .register through Sub.handle adds the str overload to that one shared registry, so Base().handle("x") picks it up too, even though nothing about Base was touched by name. No error, no warning -- the base class's behavior just changed.

2. The fix: give the subclass its own singledispatchmethod

from functools import singledispatchmethod

class Base:
    @singledispatchmethod
    def handle(self, arg):
        return f"base default {arg!r}"

    @handle.register
    def _(self, arg: int):
        return f"base int {arg}"

class Sub(Base):
    @singledispatchmethod
    def handle(self, arg):
        return f"base default {arg!r}"

@Sub.handle.register
def _(self, arg: str):
    return f"sub str {arg}"

print(Base().handle("x"))
print(Sub().handle("x"))
base default 'x'
sub str x

Measured: once Sub redeclares its own @singledispatchmethod handle (even with an identical body), Sub.handle is a distinct descriptor with its own dispatch registry. Registering str on Sub.handle now only affects Sub -- Base().handle("x") falls through to the default, unaffected.

3. Why this is easy to miss in review

A class that doesn't override a method at all reads as "inherits the base behavior, unmodified" -- that's true for ordinary methods. singledispatchmethod breaks that intuition only at the moment someone calls .register through the subclass's name, which looks like a scoped, subclass-local change. The dispatch table actually lives on the single underlying function object shared by every class in the hierarchy that didn't explicitly redeclare the decorator, so the mutation is global to all of them. Reviewing a diff that only touches Sub gives no visual signal that Base's runtime behavior just changed.

Frequently asked questions

Does registering a type on a subclass's singledispatchmethod affect the base class?

Yes, if the subclass inherited the singledispatchmethod without redeclaring it. We measured Sub.handle.register(...) adding a str overload that Base().handle() also picked up, because Sub.handle and Base.handle were the same object with one shared dispatch registry.

How do I register a type on a subclass without affecting the base class?

Redeclare @singledispatchmethod on the subclass for that method name, even with an identical default body, before calling .register on it. We measured this giving the subclass its own independent dispatch registry, isolated from the base class.

Is this specific to functools.singledispatchmethod, or does plain functools.singledispatch have the same issue?

The same root cause -- one shared dispatch registry per function object -- applies anywhere a singledispatch-based callable is referenced through more than one name without being redefined; it shows up with singledispatchmethod specifically because subclassing without overriding is the common case where two names end up pointing at the same object.

Does Python raise any warning when this happens?

No. We measured no exception and no warning in either direction -- registering through the subclass's inherited method silently changes dispatch for the base class and any other subclass that also just inherits it, with no signal in the code that looks at the subclass alone.

Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in functools.singledispatchmethod: defined a base class method with singledispatchmethod plus an int overload, created a subclass that does NOT redefine the method, called .register on the subclass's (inherited) method to add a str overload, then called the method on instances of both the base class and the subclass to see which ones picked up the new overload. PlainHeaders is our extension; Python's functools module is built in.