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
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
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.
Classes
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
Reach a function to hand around through the functions namespace:
Attributes
functions
property
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__
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 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
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
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
methods
property
methods: tuple[Method, ...]
A snapshot of the registered methods, in registration order.
Methods:
register
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 |
required |
Returns:
| Type | Description |
|---|---|
Callable
|
The registered callable, returned the same way in both
forms, so that |
from_mapping
classmethod
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.
__call__
Select a method by the arguments' runtime types, then run it.
dispatch
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
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 |
UNSET
|
ambiguity
|
str
|
What to do when two or more methods are still tied after
every tie-break. |
'raise'
|
candidates
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
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
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
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
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
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__
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
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
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
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.
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
|
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
|
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 |
lineno |
int or None
|
The line where the callable's |
Attributes
Methods:
__call__
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
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
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 |
required |
kind
|
Any
|
One of |
required |
default
|
Any
|
The parameter's default value, or |
_empty
|
Attributes:
| Name | Type | Description |
|---|---|---|
required |
bool
|
Whether the parameter has no default value. |
Attributes
Signature
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
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
|
None
|
|
varkw
|
Any
|
The |
None
|
Attributes
parameters
property
A read-only, name-keyed view of the parameters, in order.
varargs
property
varargs: Any
varkw
property
varkw: Any
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
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
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.
shape
staticmethod
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 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.
applies_to_values
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.
applies_to_hints
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
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.
same_as
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.
Hint
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
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
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