Skip to content

Comparison with other dispatch systems

bagof.dispatchers is one of several ways to get multiple dispatch in Python, and Python is one of several languages where multiple dispatch is available at all. This page compares its behaviour with three other Python libraries, plum, multipledispatch, and the standard library's functools.singledispatch, and with Julia, whose built-in multiple dispatch is the design this package takes as its reference point. The claims about bagof.dispatchers below were checked by reading its source and running the examples. The claims about the other systems reflect their current, published behaviour, checked the same way wherever that was possible, and stated narrowly where it was not.

At a glance

functools.singledispatch multipledispatch plum Julia bagof.dispatchers
Dispatch
Arguments dispatched on first only all positional all all all
Reads the wider hint vocabulary (Union, generics, Literal, TypedDict) Union only no most, not TypedDict n/a, own type system yes
Registration style decorator, reads the def decorator, types given as arguments decorator, reads the def function f(x::Int) decorator, reads the def, or explicit hints
Ambiguity
An ambiguous call n/a; rare ABC conflicts raise RuntimeError silent pick error error error
Deliberate tie-break keyword n/a none precedence — priority
Generics and values
Reads a container's contents at the call n/a n/a yes no no
Parametric types invariant n/a n/a no, covariant by content yes yes
Exact, Super and Between: exclude subclasses, or bound a value's class from below no no no no; T >: C with a concrete C only matches C yes
Around the edges
Function namespacing across modules n/a, no shared registry shared global by default shared global by default n/a, module system per module by default; shared via Dispatcher
Runtime dependency none none beartype, rich n/a none beyond typing_extensions

Julia

Julia dispatches every function call on the runtime types of all of its arguments, not only the first, and chooses the most specific method whose declared parameter types accept them. An unannotated parameter accepts any value, the same as a parameter typed Any. When a call has more than one applicable method and neither is more specific than the other, Julia does not guess. It raises a MethodError reporting the ambiguity rather than picking one of the tied methods, and bagof.dispatchers follows this model closely enough that the resemblance is intentional. Dispatch reads every argument, the most specific method wins, and a genuine tie raises AmbiguousMethodError instead of choosing arbitrarily.

Julia's parametric types are also invariant by default. Vector{Int} is not a subtype of Vector{Real}, even though Int is a subtype of Real. Two methods written for different type arguments of the same parametric type are therefore themselves incomparable, unless the type parameter is explicitly declared covariant. This is the same rule bagof.dispatchers applies to an invariant type variable, described in Variance, and the reason List[int] and List[bool] do not order one way or the other here either.

The two systems differ where the underlying platforms differ. Julia's dispatch is a language feature backed by a compiler that specializes and compiles a distinct method body for each concrete combination of argument types it actually sees. bagof.dispatchers is an ordinary library instead. It resolves the applicable method by comparing type hints at call time, and caches the result per call shape and argument type. It runs on the standard CPython interpreter, though, and generates no specialized code of its own.

functools.singledispatch

functools.singledispatch, added to the standard library by PEP 443, dispatches on the type of a function's first argument only, leaving every other parameter untyped from the dispatcher's point of view. Because only one argument matters, there is no notion of two methods being incomparable across different positions, and so no ambiguity error to raise. The implementation for the most specific registered class in the argument's method resolution order is chosen instead, following the same rule isinstance would.

>>> from functools import singledispatch
>>> @singledispatch
... def area(shape):
...     raise TypeError("unsupported shape")
>>> @area.register
... def _(shape: int) -> int:
...     return shape * shape
>>> area(4)
16

Registering against a plain class, or a Union of classes on a recent enough Python, works well. Registering against a parametrised generic such as typing.List[int] does not, since singledispatch requires each registered annotation to be an actual class. bagof.dispatchers reads the full vocabulary of type hints at every argument position instead. A signature can narrow on a union, a generic, or a TypedDict, at more than one parameter at once, none of which singledispatch was designed to express.

multipledispatch

multipledispatch is closer in spirit: it dispatches on the runtime types of every positional argument, using its own @dispatch(int, int) style of registration. Where it diverges from both Julia and bagof.dispatchers is in what happens when two registered signatures are equally specific for the same call. Registering the second of such a pair produces an AmbiguityWarning at definition time, but the warning is only advisory. A later call that actually lands on the tied signatures does not raise, and multipledispatch picks one of them without telling the caller which. There is no keyword to break such a tie deliberately, so the only recourse is to add a third, strictly more specific signature that covers the ambiguous case. bagof.dispatchers raises AmbiguousMethodError at the call itself, and a priority keyword lets an overload win a tie on purpose. Instead of warning at definition time, ambiguities() lists the tied pairs on request, so that a test suite can check for them; see When two overloads are equally specific.

multipledispatch also defaults to a single namespace shared by the whole process. @dispatch with no further argument keys a function by its bare name alone. Two unrelated modules that happen to define a same-named function under that decorator therefore silently extend one shared function, unless the caller passes an explicit namespace=. @dispatch in bagof.dispatchers defaults the other way, keeping every module's functions of the same name separate. Sharing a function across modules is instead an explicit choice, made by building a Dispatcher; see Your own registry.

plum

Of the three libraries, plum reads the closest to the same vocabulary of hints that bagof.dispatchers does. Registered signatures may use Union, Literal, and typing generics, and calls that tie between equally specific signatures raise an AmbiguousLookupError rather than resolving silently, much like bagof.dispatchers' own AmbiguousMethodError. plum also has its own tie-breaking keyword, precedence, which plays the same role as bagof.dispatchers' priority.

plum achieves this vocabulary by building on beartype, a runtime type-checking library, and the two projects take different views of what a container's declared type argument means. Registering List[int] and List[str] overloads in plum, and calling with a mixed list such as [1, "a"], raises a lookup error. plum inspects the list's own elements at the call, and finds that neither annotation matches every one of them. bagof.dispatchers never inspects a container's contents. Two overloads written for List[int] and List[bool] are equally specific for a plain list regardless of what it holds, because an invariant type variable makes the two hints incomparable. bagof.dispatchers instead reads the parametrisation a value declared when it was constructed, the way an instance of class IntBox(Box[int]) does, or one built directly as Box[int](); see Variance for the full account.

The two libraries also part ways over an arbitrary user-defined Generic class with a variance-flagged type variable, rather than one of plum's own @parametric types or a generic beartype resolves on its own. Asked to compare two parametrisations of such a class, plum currently warns that it could not resolve or classify the hint, and falls back to a best-effort answer. bagof.dispatchers reads the declared variance of the type variable directly, and orders the two parametrisations accordingly with no fallback involved.

Neither plum nor the other two libraries has an equivalent of Exact. All three inherit Python's ordinary subclass relationship, so a value of a subclass matches whichever overload its base class would. None of them offers a way to write an overload that excludes the subclasses of the class it names; see Exact types for why that distinction sometimes matters.

In summary

The three Python libraries agree with bagof.dispatchers on the basic shape of multiple dispatch. Each collects several implementations under one name and chooses the most specific one by comparing the runtime types of the arguments. bagof.dispatchers treats an ambiguous call as always an error, never a silent choice, and reads a value's declared parametrisation for a generic rather than inspecting a container's contents. It also offers Exact, for the cases where a subclass should not inherit a base class's overload, and it depends on nothing beyond typing_extensions, so it carries no runtime type-checking library of its own.