Skip to content

bagof.dispatchers

Multiple dispatch for Python, driven by ordinary type hints.

A function built by bagof.dispatchers can hold several implementations side by side, each written for a different combination of argument types. Calling the function inspects the runtime types of the arguments actually given, finds every implementation whose parameter hints accept them, and runs whichever of those candidates describes the arguments most specifically. When two candidates are equally specific and neither is more applicable than the other, the call fails with a clear error instead of picking one arbitrarily. The specificity comparison is not limited to plain classes: unions, literals, generic containers, TypedDict, TypeVar and the rest of the typing vocabulary all take part in it.

The dispatch decorator is the usual way to add an implementation to such a function. Dispatcher is the object underneath it, for building one directly rather than through the decorator. Exact marks a parameter that must match a type precisely, so that an implementation written for a base class does not also catch its subclasses. Super is its mirror image inside Type and Hint: it accepts a class or a hint together with everything above it, rather than everything below. Between, written in the same place, accepts the classes or hints that lie between a lower and an upper bound. The subtype relation over hints, and the lower-level introspection it is built from, live separately in bagof.dispatchers.core, since the rest of the bagof family depends on that comparison without needing dispatch itself. The reasoning behind the model is written up in the design RFC under docs/rfc/.

Attributes

dispatch module-attribute

dispatch = _ModuleDispatcher()

The ready-made registry most code uses for multiple dispatch.

Decorating a def with @dispatch turns it into a registered overload; a second def sharing its name in the same module becomes a second overload of the same function rather than replacing the first. Either way, @dispatch hands back the Function the overload belongs to, so the decorated name stays bound to the dispatched function instead of to one individual implementation. Functions are keyed by their defining module together with their qualified name, so the same name defined in a different module names a function of its own.

Example

>>> from bagof.dispatchers import dispatch
>>> @dispatch
... def describe(x: int) -> str:
...     return "an integer"
>>> @dispatch
... def describe(x: str) -> str:
...     return "a string"
>>> describe(7)
'an integer'
>>> describe("hi")
'a string'

The overlay form lays explicit hints over a function's parameters instead of reading them from its annotations, with positional hints given as a tuple and named hints as a dict, alongside priority as a keyword option.

Example

>>> @dispatch((int,), {"scale": int})
... def scaled(value, scale):
...     return value * scale
>>> scaled(3, scale=4)
12

Classes

Dispatcher

Dispatcher()

A registry that groups overloads into named Functions.

Decorating an ordinary def with a Dispatcher registers it as an overload rather than binding the name to that one plain function; a second def given the same name adds a second overload to the same name instead of replacing the first. In both cases the decorator hands back the Function the overload joined, so the decorated name ends up bound to the dispatched function rather than to the last individual implementation.

A Dispatcher built this way keys each function purely by its qualified name, with no reference to the module it was registered from, so several modules that register under the same name all contribute overloads to the same shared function.

Example

>>> from bagof.dispatchers import Dispatcher
>>> shapes = Dispatcher()
>>> @shapes
... def area(w: int, h: int) -> int:
...     return w * h
>>> @shapes
... def area(r: float) -> float:
...     return 3.14159 * r * r
>>> area(3, 4)
12
>>> area(2.0)
12.56636

Reach a function to hand around through the functions namespace:

Example

>>> shapes.functions["area"] is area
True
>>> "area" in shapes.functions
True

Attributes

functions property
functions: _Functions

The namespace through which this dispatcher's functions are reached.

d.functions.area and d.functions["area"] both return the function named "area", creating an empty one first if none exists yet under that name. The view defines no named method of its own, so no function name can ever collide with one.

Attribute access skips names beginning with _, so that a REPL or a tool probing for dunder attributes never creates an empty function as a side effect. Item access carries no such restriction, so d.functions["_x"] still names a function whose own name starts with an underscore. This is why an anonymous overload, such as @dispatch def _ or a bare lambda, should be avoided on the module-level dispatch registry. Every anonymous overload shares the same "_" or "<lambda>" name and so collapses into one function regardless of where it was written. Giving each overload a real name, or overlaying hints onto a named def, avoids that collision.

Methods:

__call__
__call__(*args: Any, **options: Any) -> Any

Register an overload, or return a decorator that does.

Applied bare to a def, as @d, it registers that function immediately, dispatched on the parameter hints it already carries, and returns the Function it now belongs to. Applied with an overlay of hints instead, as @d((int,), {"scale": float}), or with options such as @d(priority=5), it returns a decorator, which performs the registration once it is applied to the function, using whichever hints were laid over its parameters.

The overlay follows the same convention as Function.register: positional hints go in a tuple, named hints in a dict, and any keyword argument is always an option (currently only priority) rather than a hint.

register
register(*args: Any, **options: Any) -> Any

Register an overload, or return a decorator that does.

Calling register has exactly the same effect as calling the dispatcher directly with @d; see __call__ for the full behaviour. It hands back the Function the overload just joined, so a decorated name is bound to the dispatched function rather than to the plain implementation underneath it.

clear_cache
clear_cache() -> None

Drop every function's dispatch cache.

Registration already invalidates a function's cache automatically, and so does a change to the ABC registry, so this method is rarely needed in practice. It stays available for the remaining case: a change to what a type is applicable to that nothing else in the dispatcher observes on its own.

AmbiguousMethodError

AmbiguousMethodError(message: str, *, function: str | None = None, call: Any = None, candidates: Sequence[Any] = ())

Bases: DispatchError

Raised when more than one method matches a call equally well.

This happens when two methods both accept the call and neither is more specific than the other, so picking one over the other would depend on the order the methods happened to be registered in rather than on anything about their signatures. Dispatch refuses to make that arbitrary choice and raises instead, listing the competing methods and suggesting a signature specific enough to take precedence over both.

The competing methods are the ones still tied after every tie-break that dispatch applies, so a method that was equally specific but lost on priority, for example, is not among them. The error's candidates attribute holds these methods in registration order, which is the same tuple that Function.bestcandidates returns for the same call.

DispatchError

DispatchError(message: str, *, function: str | None = None, call: Any = None, candidates: Sequence[Any] = ())

Bases: TypeError

The base of both errors a dispatch call can raise.

A call reaches DispatchError when it cannot be pinned down to exactly one method, whether because none matched or because several matched equally well; NoMethodError and AmbiguousMethodError cover those two cases respectively. Inheriting from TypeError keeps a dispatch failure inside the exception hierarchy that an ordinary Python call already raises for a type mismatch, so a broad except TypeError: around a call needs no change to keep working.

Attributes:

Name Type Description
function str or None

The name of the function that was called.

call Any

A description of the call: the argument types for a value dispatch, or the query hints for a hint resolution.

candidates tuple

The methods, or registry keys, that competed or came closest.

NoMethodError

NoMethodError(message: str, *, function: str | None = None, call: Any = None, candidates: Sequence[Any] = ())

Bases: DispatchError

Raised when the call matches none of a function's methods.

A method can fall short of a call in two different ways before dispatch ever gets to compare types. It can fail to bind the arguments at all, for instance because it takes no such keyword, is missing a required argument, or was handed more positional arguments than it accepts. Or it can bind successfully and then reject one of the argument values against its declared types. Either way, the message names the function, describes the call by argument type, and lists whichever methods came closest, marking the argument that defeated each one with a leading !.

Function

Function(name: str | None = None)

A single named callable backed by several type-selected methods.

A Function starts out empty. Each method registered on it adds one more implementation to choose from, and calling the Function itself picks whichever registered method's parameter types best fit the arguments given, then runs it.

Example

>>> f = Function("area")
>>> _ = f.register(lambda shape: 3.14)     # a fallback for anything
>>> def rect(w: int, h: int) -> int: return w * h
>>> _ = f.register(rect)
>>> f(3, 4)
12

Parameters:

Name Type Description Default
name str | None

The function's name, shown in its error messages. When left unset, the name is taken instead from the first method registered on it.

None

Attributes:

Name Type Description
methods tuple

The registered methods, in the order they were registered. This tuple is a read-only snapshot, not a live view.

Attributes

name property
name: str

The function's name, or a placeholder if none was ever given.

methods property
methods: tuple[Method, ...]

A snapshot of the registered methods, in registration order.

Methods:

register
register(*args: Any, **options: Any) -> Any

Register a method, or return a decorator that does.

register is called in one of two distinct forms, decided by its first argument, since a type is both a callable and a valid hint and the two forms are never mixed together in one call.

The implementation form, f.register(impl), takes any callable as impl: a plain function, a class (dispatched on its __init__ or __new__), or an arbitrary callable instance. Its signature is read straight off impl, so f.register(int) registers int itself as an implementation dispatched on its constructor, rather than reading int as a hint.

The hint-overlay form instead takes a tuple of positional hints, a dict of named hints, or both, and returns a decorator. That decorator overlays the given hints onto the wrapped function's own parameters when it is applied, keeping that function's names, parameter kinds, and defaults unchanged. @f.register((int, float)) supplies positional hints, always as a tuple even when there is only one, as in (int,); @f.register({"scale": float}) supplies named hints; @f.register((int,), {"scale": float}) supplies both together; and @f.register() supplies none at all, registering the wrapped function under its own existing signature.

Which form applies is decided purely by the type of the first argument: a tuple or dict selects the hint-overlay form, any other value selects the implementation form, and no argument at all gives the hint-overlay decorator with nothing to overlay.

Named hints always belong in the dict, never as keyword arguments to register itself. A keyword argument there is a registration option instead, and the only one currently understood is priority, so f.register(int, priority=5) registers int at priority 5, while f.register(scale=float) is rejected with an error pointing at f.register({"scale": float}) instead.

Registering a method whose signature, as written, exactly matches one already registered replaces that earlier method and issues a RuntimeWarning. This is the situation a module reload or an accidentally doubled decorator produces.

Parameters:

Name Type Description Default
priority

A tie-break considered ahead of type-based specificity: between two methods a call would otherwise match equally well, the one with the higher priority is chosen. Defaults to 0, and is given as a keyword alongside either form.

required

Returns:

Type Description
Callable

The registered callable, returned the same way in both forms, so that f.register(fn), @f.register, and @f.register((int,)) all leave the decorated name bound to the dispatched function rather than to the plain implementation, following the functools.singledispatch convention.

from_mapping classmethod
from_mapping(mapping: Mapping[Any, Callable[..., Any]], *, name: str | None = None) -> Function

Build a function whose methods come straight from a mapping.

Each key in mapping describes one method's signature, and the value paired with it is the callable that method runs. A tuple key supplies one positional hint per element; a single hint used as a key by itself supplies one positional hint; and a Signature given directly as a key is used exactly as it is.

Example

>>> f = Function.from_mapping({(int,): abs, (str,): len}, name="f")
>>> f(-3), f("abcd")
(3, 4)
__call__
__call__(*args: Any, **kwargs: Any) -> Any

Select a method by the arguments' runtime types, then run it.

dispatch
dispatch(*args: Any, **kwargs: Any) -> Method

Select the method these arguments call for, without running it.

NoMethodError is raised when no method accepts the call, and AmbiguousMethodError when two methods accept it equally well.

resolve
resolve(*hints: Any, default: Any = UNSET, ambiguity: str = 'raise', **named_hints: Any) -> Any

Select the method that a call described by hints would call for.

resolve mirrors dispatch at the level of hints rather than values: each argument is given as a type hint standing for a whole range of possible values, and selection compares those hints through the sub-hint relation instead of checking actual arguments. It returns the Method selected this way.

As a lookup convenience, the same one that resolve_hint grants and matching RFC 0001 §4, a method whose parameter is Exact[C] can also be reached by a plain C query, even though C on its own is not actually a sub-hint of Exact[C]. This convenience changes which methods a hint query is considered to reach; it changes neither the sub-hint relation itself nor the specificity order between methods.

No such convenience exists for Super or Between. A plain C query stands for the instances of every subclass of C as well, which a lower bound does not accept, so a method for Super[Dog] is reached by a query such as Exact[Animal] or Between[Dog, Animal], and not by Animal.

Parameters:

Name Type Description Default
default Any

Returned instead of raising NoMethodError when no method applies to the given hints.

UNSET
ambiguity str

What to do when two or more methods are still tied after every tie-break. "raise", the default, raises AmbiguousMethodError. "warn" instead takes whichever of the tied methods was registered first and warns about the choice. "ignore" makes that same choice silently. A method that already lost a tie-break, such as one with a lower priority, is never taken.

'raise'
candidates
candidates(*args: Any, **kwargs: Any) -> tuple[Method, ...]

List every method that accepts this call, most specific first.

Where dispatch narrows a call down to the one method it selects, candidates reports every registered method whose signature accepts the arguments given, ordered from the most specific to the least specific. The methods that bestcandidates returns for the same call always come first, so the method that dispatch would select, when there is one, is the first element.

After the best candidates, the order is built in layers. The first layer holds the applicable methods that no other applicable method is more specific than. The second layer holds the methods that become most specific once the first layer is set aside, and so on until every applicable method has been placed. Within a layer, the methods that are not among the best candidates are ordered by descending priority and then by registration order. A method therefore always follows every method that is strictly more specific than it for this call. Two methods that are incomparable, with neither more specific than the other, can appear in either order, and their relative position is not a claim about specificity.

A call that no method accepts gives an empty tuple rather than raising NoMethodError, and an ambiguous call lists its tied methods first rather than raising AmbiguousMethodError.

Example

>>> class Animal: pass
>>> class Dog(Animal): pass
>>> speak = Function("speak")
>>> @speak.register
... def anything(x: object) -> str: return "..."
>>> @speak.register
... def animal(x: Animal) -> str: return "hello"
>>> @speak.register
... def dog(x: Dog) -> str: return "woof"
>>> [m.name for m in speak.candidates(Dog())]
['dog', 'animal', 'anything']
>>> [m.name for m in speak.candidates(Animal())]
['animal', 'anything']
>>> Function("empty").candidates(Dog())
()

Returns:

Type Description
tuple of Method

The applicable methods, most specific first.

itercandidates
itercandidates(*args: Any, **kwargs: Any) -> Iterator[Method]

Iterate over the methods that accept this call, most specific first.

The iterator yields the same methods, in the same order, as candidates returns for the same call. Which methods accept the call is worked out when itercandidates is called, so a method registered while the iteration is under way does not appear in it. Only the ordering of the less specific methods is deferred until the iteration reaches them, which saves that work when only the first few methods are wanted.

bestcandidates
bestcandidates(*args: Any, **kwargs: Any) -> tuple[Method, ...]

List the methods left standing once every tie-break is applied.

bestcandidates runs the same selection as dispatch but reports its outcome instead of acting on it. Among the methods that accept the call, it keeps the most specific ones and then applies the tie-breaks that dispatch applies, in the same order: an explicit priority, the argument's own MRO, how tightly each signature fits the call, and repeated TypeVars. The methods that remain are returned in registration order.

The length of the result tells which way dispatch would go. A single method is the one that dispatch returns. Several methods are the ones still tied, which dispatch reports by raising AmbiguousMethodError; the error's candidates attribute holds exactly these methods, in the same order. An empty tuple means that no method accepts the call at all, the case in which dispatch raises NoMethodError. bestcandidates itself raises neither error.

Example

>>> class Animal: pass
>>> class Dog(Animal): pass
>>> meet = Function("meet")
>>> @meet.register
... def dog_first(a: Dog, b: Animal) -> str: return "first"
>>> @meet.register
... def dog_second(a: Animal, b: Dog) -> str: return "second"
>>> [m.name for m in meet.bestcandidates(Dog(), Dog())]
['dog_first', 'dog_second']
>>> [m.name for m in meet.bestcandidates(Dog(), Animal())]
['dog_first']
>>> meet.bestcandidates(Animal(), Animal())
()

Returns:

Type Description
tuple of Method

The single selected method, the methods still tied, or no method at all.

iterbestcandidates
iterbestcandidates(*args: Any, **kwargs: Any) -> Iterator[Method]

Iterate over the methods left standing once every tie-break is applied.

The iterator yields the same methods, in the same order, as bestcandidates returns for the same call, and the selection behind it is made in full when iterbestcandidates is called.

resolve_candidates
resolve_candidates(*hints: Any, **named_hints: Any) -> tuple[Method, ...]

List every method that a call described by hints would reach.

resolve_candidates is to resolve what candidates is to dispatch. Each argument is given as a type hint rather than a value, and the result lists every method that resolve would consider for those hints, most specific first. Those methods include any that resolve reaches through its lookup convenience, by which a plain C query reaches a method whose parameter is Exact[C].

The order is built the same way as the order of candidates, with the methods that resolve_bestcandidates returns first. A hint query has no argument values, so the tie-break on an argument's own MRO plays no part, exactly as in resolve. A query that no method accepts gives an empty tuple rather than raising NoMethodError.

Example

>>> class Animal: pass
>>> class Dog(Animal): pass
>>> speak = Function("speak")
>>> @speak.register
... def anything(x: object) -> str: return "..."
>>> @speak.register
... def dog(x: Dog) -> str: return "woof"
>>> [m.name for m in speak.resolve_candidates(Dog)]
['dog', 'anything']
>>> [m.name for m in speak.resolve_candidates(Animal)]
['anything']

Returns:

Type Description
tuple of Method

The methods the hints reach, most specific first.

resolve_bestcandidates
resolve_bestcandidates(*hints: Any, **named_hints: Any) -> tuple[Method, ...]

List the methods left standing when hints are resolved.

resolve_bestcandidates is to resolve what bestcandidates is to dispatch: it runs the selection that resolve runs for a call described by hints and reports the methods that survive every tie-break, in registration order. A single method is the one that resolve returns. Several methods are the ones that resolve(..., ambiguity="raise") reports in AmbiguousMethodError, and the first of them is the method that ambiguity="warn" and ambiguity="ignore" take. An empty tuple means that no method accepts the hints, the case in which resolve raises NoMethodError or returns its default.

Example

>>> class Animal: pass
>>> class Dog(Animal): pass
>>> meet = Function("meet")
>>> @meet.register
... def dog_first(a: Dog, b: Animal) -> str: return "first"
>>> @meet.register
... def dog_second(a: Animal, b: Dog) -> str: return "second"
>>> [m.name for m in meet.resolve_bestcandidates(Dog, Dog)]
['dog_first', 'dog_second']
>>> [m.name for m in meet.resolve_bestcandidates(Dog, Animal)]
['dog_first']

Returns:

Type Description
tuple of Method

The single selected method, the methods still tied, or no method at all.

__get__
__get__(instance: Any, owner: type | None = None) -> Any

Bind the function as an instance method, self filling argument 0.

Accessing this through an instance returns a bound view that inserts that instance ahead of every call's own arguments, so a Function used as a class attribute dispatches on self (unannotated, and therefore matched against Any) together with whatever else was passed. Accessing it through the class itself, with no instance, returns the function unchanged.

ambiguities
ambiguities() -> list[tuple[Method, Method]]

List the pairs of methods that some call cannot choose between.

Registering an overload never checks it against the others, so an ambiguity otherwise surfaces only when a call that matches two methods equally well raises AmbiguousMethodError. ambiguities looks for such calls ahead of time. It is the audit to run in a test suite, where assert not f.ambiguities() fails as soon as a newly registered overload leaves some call without a most specific method.

A pair of methods is reported when some call matches both of them and the selection that dispatch performs still cannot choose between them. The two methods must accept a common call, be equally specific or incomparable, and share the same priority and the same tightness of fit. Every other registered method takes part in the selection as well, so a pair is not reported when a third method wins every call that the two have in common, in whatever order the three were registered.

The audit has limits in both directions. It checks each pair only at the call shapes that fill one of the two methods' own parameters, so it finds the ambiguities that a straightforwardly written call would hit, but not one that is reachable only by spreading arguments into *args, or only through a subclass that does not exist yet. In the other direction, the audit can report a pair that no call actually finds tied. That happens when the calls the pair has in common are won only by several narrower methods together, none of which wins all of them alone, and when the two methods' hints share values only through a Super or Between bound, since no single hint then describes those calls.

Example

>>> f = Function("area")
>>> @f.register((float, object))
... def rank(a, b): return 1
>>> @f.register((object, float))
... def order(a, b): return 2
>>> [(a.name, b.name) for a, b in f.ambiguities()]
[('rank', 'order')]
>>> @f.register((float, float))
... def both(a, b): return 3
>>> f.ambiguities()
[]

Returns:

Type Description
list of tuple of Method

Each ambiguous pair once, both methods in registration order.

clear_cache
clear_cache() -> None

Discard the dispatch cache, forcing the next call to recompute it.

The registered methods themselves are left untouched; only the cached shape plans and per-call results are thrown away. This is rarely needed in practice, since both registering a method and an abc.register call anywhere in the program already invalidate the cache automatically. It remains available for the one case those do not cover: a value whose isinstance behaviour changed in a way the ABC cache token does not reflect.

Method

Method(function: Callable[..., Any], signature: Signature | None = None, priority: int = 0)

A callable paired with the signature dispatch selects it by.

Calling a Method calls the function it wraps, so a Method can stand in for the function itself once it has been registered. Its repr prints the function's name together with its parameters and the file and line it was defined on, and a dispatch error reuses that same rendering to point a reader at the exact candidate that did, or did not, match.

Example

>>> def area(shape, scale=1.0): ...
>>> method = Method(area)
>>> method.priority
0

Parameters:

Name Type Description Default
function Callable[..., Any]

The callable this method runs when it is called or selected.

required
signature Signature | None

The signature dispatch reads the parameter types from to decide whether this method matches a call, and how specific it is compared to another. When omitted, it is obtained from function with Signature.from_callable.

None
priority int

A tie-break considered ahead of specificity: given two methods that a call matches equally well by type, the one with the higher priority is chosen. Defaults to 0.

0

Attributes:

Name Type Description
signature Signature

The method's signature.

function Callable

The wrapped callable.

priority int

The tie-break priority given at construction.

filename str

The file the wrapped callable was defined in, or "<module>" when no source file could be found for it.

lineno int or None

The line where the callable's def begins, when that could be determined.

Attributes

name property
name: str

The wrapped function's __name__, or a placeholder if it has none.

location property
location: str

A short file:line string pointing at the function's definition.

Methods:

__call__
__call__(*args: Any, **kwargs: Any) -> Any

Run the wrapped function, forwarding every argument to it.

describe
describe(highlight: Collection[Any] | None = None) -> str

Render the method's name, signature and definition site.

This is the same rendering repr produces, except that the hint of each parameter named in highlight is prefixed with a !, marking it as the argument responsible for the method not matching in a dispatch error. A parameter is named either by its own name or, for the catch-all parameters, by Parameter.VAR_POSITIONAL or Parameter.VAR_KEYWORD.

Parameter

Parameter(name: str, hint: Any, kind: Any, default: Any = _empty)

One parameter of a Signature.

A Parameter is immutable once constructed and holds its name, its type hint, its kind (how it may be passed to a call), and its default value. A parameter that has no default value is required.

Example

>>> p = Parameter("x", int, Parameter.POSITIONAL_OR_KEYWORD)
>>> p.name, p.required
('x', True)

Parameters:

Name Type Description Default
name str

The parameter's name.

required
hint Any

The type hint dispatch reads for this parameter. Leaving a parameter unannotated gives it Any, so it accepts any argument.

required
kind Any

One of Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD or Parameter.KEYWORD_ONLY, mirroring inspect.Parameter. The *args and **kwargs hints are not represented this way; they live directly on the signature instead.

required
default Any

The parameter's default value, or Parameter.empty when it is required.

_empty

Attributes:

Name Type Description
required bool

Whether the parameter has no default value.

Attributes

required property
required: bool

Report whether the parameter has no default value.

Signature

Signature(parameters: Mapping[str, Parameter], varargs: Any = None, varkw: Any = None)

The set of parameters, and their hints, dispatch binds a call against.

A Signature holds its parameters in declaration order, together with the *args hint (varargs) and the **kwargs hint (varkw) when the callable it describes accepts either. Build one from an existing callable with from_callable, or, with no callable at all, straight from a list of hints with from_hints.

Every declared parameter takes part in dispatch, whether or not it carries a hint of its own. An unannotated parameter is treated as Any, so it still has to be filled for a call to bind but never narrows down which method gets chosen.

Example

>>> def area(shape, scale=1.0): ...
>>> sig = Signature.from_callable(area)
>>> list(sig.parameters)
['shape', 'scale']
>>> sig.dispatched_names
('shape', 'scale')

Build a signature from its parameters and its catch-all hints.

Parameters:

Name Type Description Default
parameters Mapping[str, Parameter]

The parameters, keyed by name, in the order they should be considered.

required
varargs Any

The *args element hint, or None when the callable takes no *args at all. An unannotated *args is represented as Any, not as None.

None
varkw Any

The **kwargs value hint, or None when the callable takes no **kwargs.

None

Attributes

parameters property
parameters: Mapping[str, Parameter]

A read-only, name-keyed view of the parameters, in order.

varargs property
varargs: Any

The *args element hint, or None if there is none.

An unannotated *args is represented as Any; None means the callable does not accept *args at all.

varkw property
varkw: Any

The **kwargs value hint, or None if there is none.

An unannotated **kwargs is represented as Any; None means the callable does not accept **kwargs at all.

dispatched_names property
dispatched_names: tuple[str, ...]

The names an argument may be given by keyword and still dispatch.

This covers the positional-or-keyword and keyword-only parameters, in order. A positional-only parameter can only be dispatched on by its position, so it never appears here.

Methods:

from_callable classmethod
from_callable(fn: Callable[..., Any]) -> Signature

Read a signature off an existing callable.

Parameter names, kinds, and defaults come from inspect.signature, while the hints themselves come from typing_extensions.get_type_hints, read with Annotated metadata preserved so that a construct such as Exact[...] survives intact. A *args: H parameter becomes the signature's varargs, a **kwargs: H parameter becomes its varkw, and the return annotation is ignored entirely. Any parameter with no annotation of its own dispatches on Any.

A hint can be a forward reference that cannot yet be resolved, for instance a name defined further down the same module or one imported only under TYPE_CHECKING. When that happens, the signature holds onto the raw annotation and resolves it the first time the signature is actually used for dispatch. A name still undefined at that point raises NameError.

from_hints classmethod
from_hints(*hints: Any, **named_hints: Any) -> Signature

Build a signature straight from hints, with no callable behind it.

This is the primitive that def-less registration forms build on, such as Function.from_mapping. Each positional hint given becomes a positional-only parameter, and each keyword hint becomes a parameter of that name that a call may fill either positionally or by keyword. Every parameter produced this way is required, with no default of its own.

Example

>>> sig = Signature.from_hints(int, scale=float)
>>> sig.dispatched_names
('scale',)
shape staticmethod
shape(args: Sequence[Any], kwargs: Mapping[str, Any]) -> tuple[int, tuple[str, ...]]

Reduce a call to its shape: how many positionals, which keywords.

Whether one signature is more specific than another can only be decided relative to a particular shape, because which parameter an argument lands in depends on how the call was actually written. Two calls that pass "the same" arguments differently, such as f(1, 2) and f(1, y=2), count as different shapes here.

bind
bind(args: Sequence[Any], kwargs: Mapping[str, Any]) -> Binding | None

Bind a call to the parameters, exactly the way Python itself would.

The [Binding][bagof.dispatchers._signature.Binding] returned on success says where every argument landed. None comes back instead when the call simply does not fit, whether because it has too many positionals for a signature with no *args, an unexpected keyword for one with no **kwargs, the same parameter filled twice, or a required parameter left with nothing to fill it. This follows inspect.Signature.bind precisely, working from the plan computed once when the signature was built.

Example

>>> sig = Signature.from_hints(int, y=int)
>>> sig.bind((1,), {"y": 2}) is None
False
>>> sig.bind((1, 2, 3), {}) is None   # too many positionals
True
applies_to_values
applies_to_values(args: Sequence[Any], kwargs: Mapping[str, Any]) -> bool

Report whether the signature accepts a call made with these values.

Three things must all hold for this to succeed. The call has to bind in the first place; every bound value then has to be an instance of whichever hint it landed in, with an unannotated catch-all accepting anything at all; and any TypeVar repeated across more than one parameter has to be solvable consistently across every value that reached it. A parameter left to its own default was never an argument of the call, so its hint plays no part in this check.

Example

>>> sig = Signature.from_hints(int, y=str)
>>> sig.applies_to_values((1,), {"y": "a"})
True
>>> sig.applies_to_values((1,), {"y": 2})
False
applies_to_hints
applies_to_hints(hints: Sequence[Any], named_hints: Mapping[str, Any]) -> bool

Report whether the signature accepts a call described by hints.

This mirrors applies_to_values at the level of hints: the call still has to bind, and every query hint then has to be a sub-hint of whatever hint it landed in, with the same consistency required of a TypeVar repeated across positions. As a lookup convenience, described in RFC 0001 §4 and shared with resolve_hint, a slot typed as Exact[C] also accepts a query equivalent to plain C, even though C by itself is not actually a sub-hint of Exact[C].

le
le(other: Signature, shape: Any) -> bool

Report whether this signature is at least as specific as other.

Both signatures are bound to the given call shape, and then, at every argument position, this signature's landed hint has to be a sub-hint of other's landed hint there, written A ⊑ B. A signature unable to bind the shape at all is not comparable to the other one, and the answer comes back False in that case.

The repeated-TypeVar grouping refinement from RFC 0001 §3, where a method whose repeated TypeVars tie strictly more arguments together than another's counts as more specific, is a separate selection step living on Function rather than here. This method deliberately stays the plain, per-argument sub-hint comparison and leaves that refinement out.

Example

>>> a = Signature.from_hints(int, int)
>>> b = Signature.from_hints(int, object)
>>> shape = Signature.shape((1, 2), {})
>>> a.le(b, shape)
True
>>> b.le(a, shape)
False
same_as
same_as(other: Signature) -> bool

Report whether two signatures were written the same way, structurally.

same_as is stricter than [==][bagof.dispatchers._signature.Signature.__eq__], which holds whenever two signatures accept and order calls identically regardless of how they were spelled. Under ==, (x: T, y: T) equals (x: T, y: U), since both reduce to (Any, Any), and Exact[int] equals a TypeVar bound to int. same_as instead asks whether the two signatures were spelled identically: the same parameter names, kinds, and required-ness, with hints that match structurally, meaning TypeVars compared by identity, generic aliases compared by origin and arguments, and forward references compared by name.

This is the comparison a registry uses to decide whether a new method replaces an existing one. Only a method registered with exactly the same spelling, the shape produced by a module reload or an accidentally doubled decorator, replaces what is already there, while two methods that merely happen to be equivalent despite being written differently are both kept.

Between

Accept what lies between two bounds.

An ordinary annotation bounds what a parameter accepts from above, and Super bounds it from below. Between[L, U] bounds it from both sides at once. On a value parameter, x: Between[Dog, Animal] accepts a value whose class is Dog, Animal, or a class that derives from Animal and is itself a base of Dog. It refuses an instance of Puppy, a subclass of Dog, because Puppy lies below the lower bound, and it refuses a plain object(), because object lies above the upper bound. Inside Type, where the value passed is itself a class, Type[Between[Dog, Animal]] accepts the same classes Dog and Animal and those between them. Inside Hint, where the value passed is a type hint, Hint[Between[int, numbers.Real]] accepts the hints int, numbers.Integral and numbers.Real, and refuses bool, which lies below int, as well as object and Any, which lie above numbers.Real.

Each of these spellings describes such an interval. A plain C is the interval Between[Never, C], since it accepts C and everything below it. Super[C] is the interval from C to the top, which is object on a value and inside Type, and Any inside Hint. Exact[C] names the single class or hint C as it is written, which is narrower than Between[C, C]. On a value, Between[C, C] accepts a value whose class is equivalent to C, meaning that each of the two classes counts as a subclass of the other, which register or __subclasshook__ can arrange for a class other than C. Exact[C] accepts a value whose class is C itself.

On a value, the interval is compared against the value's class, so each bound must be a hint that a class can be compared against, such as a class, a union of classes, Never or Any. A hint that reads the value itself, such as a Literal, a TypedDict or a parametrised generic, is refused as a bound there. Inside Type or Hint, and as a type argument, the interval is compared against another hint, so any hint can be a bound. The same Between[Literal[1], int] that is refused on a value is therefore accepted inside Hint[...] and in List[...].

As the type argument of an invariant generic, where a plain argument means exactly that type, Between[L, U] names a range of arguments and stands for every parametrisation whose argument lies in it. List[Between[Dog, Animal]] accepts a list declared to hold Dog or Animal, such as an instance of class Dogs(List[Dog]), and refuses one declared to hold Puppy. A value that declares nothing, such as a plain [Dog()], matches as it matches every parametrisation of List. At a covariant position, such as the argument of Sequence, the plain argument U already accepts every parametrisation below it, so Sequence[Between[Never, U]] means the same as Sequence[U] and is read that way, while a lower bound there would be ignored and is refused with a message naming Sequence[U]. A contravariant position is the mirror image, where an upper bound is refused. A bound is also refused as an element of a Tuple and in the signature of a Callable, whose positions have a fixed variance of their own.

One interval is more specific than another when it lies inside it, so Between[Dog, Animal] is more specific than Super[Dog] and than Animal. On the other hand, Dog and Between[Dog, Animal] are not ordered against each other, although both accept a value of class Dog, so a call with such a value is ambiguous when methods are registered for both, and a priority on either method settles it. The same holds inside Type and Hint.

The lower bound must be a sub-hint of the upper bound. An empty interval such as Between[Animal, Dog] is refused when it is written, with a message that suggests the reversed spelling when that one is not empty. Between[Any, U] is refused as well unless U is Any itself, because nothing else lies above Any. A TypeVar written as an end of a bound is read as its own bound, so an unbounded T there means Any. Super[T] then accepts no value, and List[Super[T]] accepts only a list that declares no argument. The interval with no lower bound is spelled Between[Never, U]. On a value and inside Type or Hint it means the same as plain U, while as the type argument of an invariant generic it accepts every argument up to U, where plain U accepts U alone. A lower bound cannot be a quoted forward reference, because nothing would ever resolve it, but quoting the whole annotation works as usual.

Exact, Super and Between cannot be nested inside one another, even through a union or a TypeVar, and inside Type or Hint a bound is always written inside the brackets, as in Type[Between[L, U]], never around them. A type checker reads Between[L, U] as Union[L, U, Any], which accepts every call the runtime accepts on a value and inside Type or Hint. As a type argument of an invariant generic, mypy reads each member of that union as a lower bound, so it cannot check the upper end, and it rejects a valid call whose argument lies below U, while pyright accepts every call there. For a bound with no lower end, a TypeVar bounded by U, as in List[T], is read by dispatch exactly as List[Between[Never, U]] and is checked by both tools.

Example

>>> from typing import Type
>>> from bagof.dispatchers import Between
>>> from bagof.dispatchers.core import ishintstance
>>> class Animal: pass
>>> class Dog(Animal): pass
>>> class Puppy(Dog): pass
>>> ishintstance(Dog(), Between[Dog, Animal])
True
>>> ishintstance(Puppy(), Between[Dog, Animal])
False
>>> ishintstance(Animal, Type[Between[Dog, Animal]])
True
>>> ishintstance(object, Type[Between[Dog, Animal]])
False
>>> from typing import List
>>> class Dogs(List[Dog]): pass
>>> class Puppies(List[Puppy]): pass
>>> ishintstance(Dogs(), List[Between[Dog, Animal]])
True
>>> ishintstance(Puppies(), List[Between[Dog, Animal]])
False

Exact

Restrict a dispatch parameter to one type, excluding its subtypes.

Ordinary dispatch treats a parameter annotated with a class as accepting that class and anything derived from it, so that one implementation can serve a base type and every subclass alike. Exact[C] asks for the opposite: an implementation with a parameter annotated Exact[int] matches an argument whose type is exactly int, and not one whose type is bool, even though bool is an int subclass that an ordinary int parameter would gladly accept. Exact exists for the cases where a handler written for a base type would misbehave if a more specific subtype reached it.

A type checker reads Exact[C] as plain C, since it is defined as Annotated[C, EXACT] and a checker looks straight through Annotated metadata to the wrapped type.

Exact composes with Type and Hint. Placing Exact inside the bracket narrows that position from matching a subtype to matching the type itself, so Type[Exact[int]] matches the class int but not bool, and Hint[Exact[int]] matches the hint int but not bool. Writing Exact around the whole form instead, as Exact[Type[int]], means the same thing and is normalised to the inner spelling Type[Exact[int]], which is the form to prefer.

Inside those brackets, Super is the mirror image of Exact: where Type[Exact[int]] accepts the class int alone, Type[Super[int]] accepts int together with every class above it, and Between accepts the classes between two bounds. Neither can be combined with Exact, since an exact type leaves nothing above or below it to bound.

Example

>>> from bagof.dispatchers import Exact
>>> Exact[int]
typing.Annotated[int, EXACT]

Hint

Hint(*args: Any, **kwargs: Any)

Bases: Generic[_T_co]

A hint matched by the type hints that are sub-hints of its argument.

Where an ordinary annotation describes the values an argument may take, Hint[X] describes the type hints an argument may be. An argument matches Hint[X] when the argument is itself a hint and a sub-hint of X, decided by issubhint. So Hint[int] matches int and bool but not str, and Hint[tx.Union] matches any union. This lets a dispatcher select on a hint that is handed to it as a value, the way a registry or a schema builder passes types around at runtime.

The argument may itself be Exact[C], which narrows the match from "a sub-hint of C" to "the hint C itself". Hint[Exact[int]] therefore matches only int, not bool, and Hint[Exact[tx.Union]] matches only the bare Union form.

The argument may instead be Super[X], which turns the bound around: Hint[Super[int]] matches the hint int and every hint above it, such as numbers.Integral, object, and Any. It matches neither bool, which sits below int, nor the unrelated str. An argument written as Between[L, U] bounds the match from both sides, so Hint[Between[int, numbers.Real]] matches int, numbers.Integral and numbers.Real, but neither bool, which sits below int, nor object, which sits above numbers.Real.

Hint is a marker used only in annotations and is never instantiated. A bare Hint, written with no argument, is read as Hint[Any], the hint matched by every type hint.

Example

>>> from bagof.dispatchers import Hint
>>> from bagof.dispatchers.core import ishintstance
>>> ishintstance(int, Hint[int])
True
>>> ishintstance(bool, Hint[int])
True
>>> ishintstance(str, Hint[int])
False
>>> ishintstance(tx.Union[int, str], Hint[tx.Union])
True

Super

Bound a hint from below: accept a type and everything above it.

An ordinary annotation places an upper bound on what a parameter accepts, so a parameter annotated Animal accepts an instance of Animal or of any class derived from it. Super[C] places a lower bound instead. On a value parameter, x: Super[Dog] accepts a value whose class is Dog or a class that Dog derives from, such as Animal or object. It refuses an instance of a subclass of Dog, such as Puppy, and an instance of an unrelated class. Inside Type, where the value passed is itself a class, Type[Super[Dog]] accepts the class Dog and every class that Dog derives from. Inside Hint, where the value passed is a type hint, Hint[Super[int]] accepts the hint int together with every hint above it, such as numbers.Integral, Union[int, str] and Any, but not bool, which sits below it. As the type argument of an invariant generic, where a plain argument means exactly that type, List[Super[int]] accepts a list declared to hold int or a type above it, such as numbers.Integral or object, and refuses one declared to hold bool. In each position Super[C] is the range that runs from C up to the top of the order, and Between names both ends of such a range when the top should be bounded as well.

On a value, Super belongs to a family of hints that compare the value's own class against a range of classes. A plain C accepts a value whose class is C or lies below it, and Exact[C] accepts a value whose class is C itself. Super[C] accepts a value whose class is C or lies above it, and Between[L, U] accepts a value whose class lies between L and U. Writing Exact, Super or Between on a value is a deliberate departure from substitutability, the usual expectation that a function written for a class also accepts the instances of its subclasses. A method for Super[Dog] handles Dog and its ancestors and leaves every subclass of Dog to other methods, which suits a general fallback that should never capture the specialised classes. Because the bound is compared against the value's class, it must be a hint that a class can be compared against, such as a class, a union of classes, Never or Any. A hint that reads the value itself, such as a Literal, a TypedDict or a parametrised generic, is refused as the bound of a value. Inside Type[...] or Hint[...], and as a type argument, the bound is compared against another hint, so any hint can be a bound there.

As a type argument, Super[C] stands for every parametrisation whose argument lies at or above C. A value that declares its arguments, such as an instance of class IntList(List[int]), matches when its declared argument lies in that range, and a value that declares nothing, such as a plain [1], matches as it matches every parametrisation of List. Each bound names its own range and does not reach through the position around it, so List[List[Super[int]]] accepts a list declared to hold List[Super[int]] and not one declared to hold List[int]. A lower bound is meaningful at an invariant position. At a covariant position, such as the argument of Sequence, the variance already accepts every parametrisation below the argument, so a lower bound would accept every sequence, and it is refused with a message naming the plain spelling to write instead. At a contravariant position the variance already reads the argument as a lower bound, so Super[C] there means the same as plain C and is read that way. A bound is also refused as an element of a Tuple and in the signature of a Callable, whose positions have a fixed variance of their own.

Super is written inside the bracket, as in Type[Super[C]]. Writing it around the whole form, as in Super[Type[C]], means the same thing and is normalised to the inner spelling. SuperType and SuperHint are shorter aliases for the two spellings.

Lower bounds are ordered in the opposite direction to the classes they name. Super[Animal] is more specific than Super[Dog], because every class above Animal is also above Dog, so the classes above Animal form the smaller set. Exact[Dog] sits below both Dog and Super[Dog], since the single class Dog belongs to each of them. A plain Animal and Super[Dog] are not ordered against each other, yet a value of class Dog or Animal matches both, so a call with such a value is ambiguous when methods are registered for both. A third method for Exact[Dog] settles the call with a Dog, and an explicit priority on either method settles every call the two share. The same order holds inside Type and Hint, where Type[Exact[Dog]] sits below both Type[Dog] and Type[Super[Dog]]. As a type argument, one range is more specific than another when it lies inside it, and a plain argument counts as a range holding only itself, so List[Dog] and List[Super[Animal]] are both more specific than List[Super[Dog]].

Super and Exact cannot be combined, in either order, because an exact type has nothing above it to bound, and neither of them can be combined with Between. A type checker reads Super[C] as Union[C, Any], which accepts every call the runtime accepts. As a type argument of an invariant generic, mypy reads that union as a lower bound, as the runtime does, so it accepts a list[int] or a list[object] passed to a List[Super[int]] parameter and rejects a list[bool]. Inside the function body, a checker treats a parameter annotated Super[Dog] as a Dog and checks attribute access against Dog, although the value may be an Animal. When that difference matters, the value is best treated as an object in the body.

Example

>>> from typing import Type
>>> from bagof.dispatchers import Super
>>> from bagof.dispatchers.core import ishintstance
>>> class Animal: pass
>>> class Dog(Animal): pass
>>> class Puppy(Dog): pass
>>> Super[int]
typing.Annotated[int, SUPER]
>>> ishintstance(Animal(), Super[Dog])
True
>>> ishintstance(Puppy(), Super[Dog])
False
>>> ishintstance(object, Type[Super[int]])
True
>>> ishintstance(bool, Type[Super[int]])
False
>>> from typing import List
>>> class IntList(List[int]): pass
>>> class BoolList(List[bool]): pass
>>> ishintstance(IntList(), List[Super[int]])
True
>>> ishintstance(BoolList(), List[Super[int]])
False

SuperHint

Accept a type hint and every hint above it.

SuperHint[X] is exactly Hint[Super[X]], the spelling Super describes, so a parameter annotated SuperHint[int] accepts the hint int and every hint that int is a sub-hint of, such as numbers.Integral or Any, but not the narrower bool. The alias always needs its bound: a bare SuperHint is refused wherever a hint is read, because the unbounded reading would be every hint, which plain Hint already spells.

Example

>>> from bagof.dispatchers import Hint, Super, SuperHint
>>> SuperHint[int] == Hint[Super[int]]
True

SuperType

Accept a class and every class it derives from.

SuperType[C] is exactly Type[Super[C]], the spelling Super describes, so a parameter annotated SuperType[Dog] accepts the class Dog and each of its base classes up to object. The alias always needs its bound: a bare SuperType is refused wherever a hint is read, because the unbounded reading would be every class, which plain type already spells.

Example

>>> from typing import Type
>>> from bagof.dispatchers import Super, SuperType
>>> SuperType[int] == Type[Super[int]]
True