skip to content

Why does @functools.singledispatch always run the fallback when applied to a method?

level: middleimportance: should knowfreq 26%

answer

  1. Look at what the first argument actually is
  2. On a method the first argument is self
  3. It fails silently, never raises
  4. There is a dedicated method variant
  5. The dispatch decorator must sit outermost

basics

~20 s

Dispatch reads the first positional argument, which on an instance method is self. Its type never varies, so every call resolves to the same implementation. Use functools.singledispatchmethod, which dispatches on the first argument after self.

solid answer

~40 s

`functools.singledispatch` keys on the first positional argument. On an instance method that argument is `self`, whose class is the same on every call, so the lookup always lands on one implementation — usually the `object` fallback — and the type-specific registrations are dead code. Nothing raises; it silently does the wrong thing. The fix is `functools.singledispatchmethod` (3.8+), a descriptor whose binding skips `self` (or `cls`) and dispatches on the next positional argument. Registration inside the class body works the same way, annotating the second parameter. It also composes with `@classmethod` and `@staticmethod`, but `singledispatchmethod` **must be the outermost decorator** — put `@classmethod` on the outside and you get `AttributeError: 'classmethod' object has no attribute 'register'`.

code

python · 22 lines
python
from functools import singledispatch, singledispatchmethod

class Importer:
    @singledispatch
    def broken(self, node):
        return "fallback"

    @broken.register
    def _(self, node: int):
        return "int"

    @singledispatchmethod
    def visit(self, node):
        return "fallback"

    @visit.register
    def _(self, node: int):
        return "int"

imp = Importer()
print(imp.broken(3))   # dispatched on self -> 'fallback'
print(imp.visit(3))    # dispatched on 3 -> 'int'

go deeper

for a junior

Remember the rule that dispatch looks at the first positional argument, and that on a method that argument is self — so there is a separate method-flavoured decorator you should reach for instead.

for a middle

Explain why the failure is silent rather than an error, show the singledispatchmethod fix, and get the decorator ordering with classmethod right including the exact AttributeError.

for a senior

Talk about catching this in review and in tests: a raising fallback, dispatch assertions, and knowing that the registry is class-wide so a subclass override replaces it wholesale.

for a principal

Weigh method-level dispatch against ordinary polymorphism for a shared codebase — when a registry inside a class is clearer than an overridden method, and when it just hides the type hierarchy from readers.

This is the single most common way a first attempt at type dispatch goes wrong, and it is nasty because it fails **silently**. ## The failure ```python from functools import singledispatch class Importer: @singledispatch def visit(self, node): return "fallback" @visit.register def _(self, node: int): return "int" Importer().visit(3) # -> 'fallback', not 'int' ``` `visit` is a generic function like any other. When you call `Importer().visit(3)`, ordinary method binding passes the instance as the first positional argument, so dispatch inspects `type(self)` — `Importer` — not `type(3)`. `Importer` matches nothing but the `object` entry, so the fallback runs. Every registration on the method is unreachable, on every call, forever. No exception, no warning; the tests that only exercise the fallback path pass. ## The fix: singledispatchmethod `functools.singledispatch` **method** (3.8+) exists precisely for this. It is a descriptor: accessing it through an instance or a class returns a bound wrapper that holds onto `self`/`cls` and dispatches on the **next** positional argument. ```python from functools import singledispatchmethod class Importer: @singledispatchmethod def visit(self, node): return "fallback" @visit.register def _(self, node: int): return "int" Importer().visit(3) # -> 'int' ``` The registration forms are the same as for the plain decorator — an annotation on the dispatched parameter (which is now the *second* parameter, after `self`), or an explicit class passed to `register`. The registry lives on the `singledispatchmethod` object created in the class body, so it is shared by all instances of the class; there is no per-instance registry, and registering at runtime from one instance affects every instance. ## Ordering with classmethod and staticmethod `singledispatchmethod` composes with the other method decorators, and the order is not symmetric: ```python class Row: @singledispatchmethod @classmethod def parse(cls, raw): return f"unhandled {type(raw).__name__}" @parse.register @classmethod def _(cls, raw: int): return f"count {raw}" ``` `singledispatchmethod` has to be **outermost**, both on the base implementation and on each registration. The reason is mechanical: `register` is an attribute of the `singledispatchmethod` object, so whatever ends up on the outside must be the object that carries it. Wrap it the other way round — ```python @classmethod @singledispatchmethod def parse(cls, raw): ... ``` — and the class body raises `AttributeError: 'classmethod' object has no attribute 'register'` at import time, because a `classmethod` object has no `register`. Getting the import-time error is the lucky case; the truly silent failure is the plain-`singledispatch`-on-a-method version above. `@staticmethod` nests the same way, and there dispatch is on the first parameter because there is no `self` to skip. ## Inheritance and overrides Dispatch and inheritance are two independent mechanisms here, and it pays to say so explicitly in an interview. A subclass that redefines the method replaces the whole generic method, registry and all — it does not "add to" the parent's registry. To extend rather than replace, register additional implementations against the parent's method object, or design the base class to expose a registration hook. There is also no way to register an implementation "for subclasses of this class, but only when called on that subclass" — the registry keys on the *argument's* type, never on the receiver's. ## How to catch it in review Two cheap habits: make the base implementation `raise TypeError(...)` rather than returning something plausible, so an unresolved type is loud rather than quietly wrong; and assert dispatch in tests via the generic function's `dispatch()` attribute with the class you expect, which fails immediately when a registration is unreachable.

  • What error do you get if you put @classmethod outside @functools.singledispatchmethod?
    `AttributeError: 'classmethod' object has no attribute 'register'`, raised while the class body executes. The `register` attribute belongs to the `singledispatchmethod` object, so it must be the outer decorator for the registrations below it to resolve.
  • Is the registry of a singledispatchmethod per instance or shared?
    Shared. The object is created once when the class body runs, so all instances — and subclasses that inherit the attribute rather than overriding it — see the same registry. Registering at runtime is a global change to the class's behaviour, which is a good reason to keep registration at import time.
  • How would you make an unreachable registration loud instead of silent?
    Make the base implementation raise `TypeError` naming the unhandled type rather than returning a plausible default, and assert in tests that the generic function's `dispatch()` returns the implementation you registered for a given class. Both turn a wrong-but-quiet result into a failure.

saying these in an interview costs you the question

  • Expects an exception when singledispatch decorates a method
  • Thinks self is skipped automatically by singledispatch
  • Puts classmethod outside the dispatch decorator
  • Believes each instance gets its own registry
  • Assumes a subclass adds to the parent's registry

context