Ambiguous registrations and surprising choices make sense once you see the resolution algorithm.
When a singledispatch function is called, it looks up type(arg) in a dispatch cache (a WeakKeyDictionary keyed by class). On a miss, it computes the best implementation:
1. It builds an MRO for the argument's class that also includes the ABCs that have registered implementations and that the class is a virtual subclass of (via register or __subclasshook__). This uses the same C3 linearisation as normal inheritance (functools._compose_mro). 2. It walks that MRO and picks the first class with a registered implementation. 3. If two ABCs match at the same level with no ordering between them, it raises RuntimeError: Ambiguous dispatch. Register an implementation for the concrete class to resolve it.
The cache is invalidated whenever you register a new implementation, and also when abc.ABCMeta's registry changes (tracked via a cache token), so registering virtual subclasses later is picked up.
Registration via annotations accepts classes and, since 3.11, X | Y unions and typing.Union; it rejects parameterised generics like list[int], because dispatch only sees runtime classes, not element types.
Dispatch is on args[0].__class__, so proxies that override __class__ dispatch as the class they claim to be. Keyword-only calls fail: there must be at least one positional argument.
Example
from collections.abc import Iterable, Sized
from functools import singledispatch
@singledispatch
def describe(obj): return "object"
@describe.register
def _(obj: Sized): return "sized"
@describe.register
def _(obj: Iterable): return "iterable"
class Box: # virtual subclass of Sized only
def __len__(self): return 1
class Bag: # virtually both Sized and Iterable
def __len__(self): return 0
def __iter__(self): return iter(())
print(describe(Box())) # sized
try:
describe(Bag())
except RuntimeError as e:
print(str(e)[:18]) # Ambiguous dispatch
describe.register(Bag, lambda obj: "bag")
print(describe(Bag())) # bag
try:
describe.register(list[int], lambda obj: "ints")
except TypeError:
print("generics rejected") # generics rejectedRemember
- Dispatch walks an MRO extended with relevant ABCs, cached per class
- Unordered matching ABCs cause 'Ambiguous dispatch'; register the concrete class
- Only runtime classes dispatch; parameterised generics are rejected
Watch out for
- Registering for
list[int]andlist[str]expecting dispatch on element types.