Exact types
By default, an overload registered for int also accepts bool, because
bool is a subclass of int. Exact[C] narrows a hint against that
default: it matches a value only when the value's type is exactly C, not
any of its subclasses:
>>> from bagof.dispatchers import dispatch, Exact
>>> @dispatch
... def label(x: Exact[int]) -> str:
... return "exactly an int"
>>> @dispatch
... def label(x: object) -> str:
... return "something else"
>>> label(5)
'exactly an int'
>>> label(True)
'something else'
True is an int, since bool is a subclass of int, so a plain int
overload would take it. Exact[int] refuses it, and the call falls through
to the object overload.
This is the reverse of the usual multiple-dispatch situation, where an
overload written for a base class is meant to also serve its subclasses.
Exact is for the times a handler written for a base type must not run on
a more specific one instead. To a static type checker, Exact[int] is
indistinguishable from plain int.
Exact inside Type and Hint
Exact also narrows a position nested inside another hint. Placing it
inside a Type[...] bracket dispatches on the class object itself,
matching one exact class rather than that class and its subclasses:
>>> from typing import Type
>>> from bagof.dispatchers import dispatch, Exact
>>> @dispatch
... def which(cls: Type[int]) -> str:
... return "int or a subclass"
>>> @dispatch
... def which(cls: Type[Exact[int]]) -> str:
... return "exactly int"
>>> which(int)
'exactly int'
>>> which(bool)
'int or a subclass'
The same composition works with Hint, where
Hint[Exact[int]] matches the hint int alone rather than every hint
below it.
Writing Exact around the whole form, as Exact[Type[int]], means
exactly the same thing; it is normalised to the inner spelling
Type[Exact[int]], which is the form to prefer when writing a
signature.
Bounds: Super and Between
A plain annotation places an upper bound on a value's class: an overload
for Animal accepts an instance of Animal or of any class derived from
it. Super[C] places a lower bound instead, and accepts a value whose
class is C or a class that C derives from, up to object.
Between[L, U] places both bounds at once, and accepts a value whose class
lies between L and U, both included. Together with Exact, these let
an overload say which classes it serves, and leave the others to other
overloads:
>>> from bagof.dispatchers import Between, Exact, Super, dispatch
>>> class Animal: pass
>>> class Dog(Animal): pass
>>> class Puppy(Dog): pass
>>> @dispatch
... def greet(x: Super[Dog]) -> str:
... return "Dog or one of its bases"
>>> @dispatch
... def greet(x: Exact[Dog]) -> str:
... return "exactly a Dog"
>>> @dispatch
... def greet(x: Puppy) -> str:
... return "a Puppy"
>>> greet(Animal())
'Dog or one of its bases'
>>> greet(object())
'Dog or one of its bases'
>>> greet(Dog())
'exactly a Dog'
>>> greet(Puppy())
'a Puppy'
Like Exact, a bound on a value is a deliberate departure from the usual
rule that code written for a class also serves its subclasses. Exact[C]
refuses the instances of every subclass of C, and Super[C] refuses the
instances of every class below C. That suits an overload meant for the
general case that must not capture the more specialised classes, which
have, or will be given, overloads of their own. Here the Super[Dog]
overload handles an Animal or a plain object, and a Puppy never
reaches it, even when no Puppy overload has been registered.
Each of these spellings compares the value's class against a range of
classes. A plain C is the range Between[Never, C], from the bottom up to
C, and Super[C] is the range Between[C, object]. Exact[C] is
narrower than Between[C, C]: Between[C, C] accepts a value whose class
is equivalent to C, which register or __subclasshook__ can arrange
for a class other than C, while Exact[C] accepts only a value whose
class is C itself. One range is more specific than another when it lies
inside it, so an overload for Between[Dog, Animal] takes Animal from
the Super[Dog] overload, while object, which lies above Animal, stays
where it was:
>>> @dispatch
... def greet(x: Between[Dog, Animal]) -> str:
... return "between Dog and Animal"
>>> greet(Animal())
'between Dog and Animal'
>>> greet(object())
'Dog or one of its bases'
>>> greet(Dog())
'exactly a Dog'
Because a bound on a value is compared against the value's class, each
bound has to be a hint that a class can be compared against, such as a
class, an abstract base class, a union of classes, a protocol with methods
only, Never or Any. A hint that is matched against the value itself,
such as a Literal, a TypedDict, a protocol with data members, or a
parametrised generic such as List[int], is refused as the bound of a
value when the overload is registered.
Bounds inside Type and Hint
The same bounds apply to a class passed as a value. A Type[C] parameter
accepts the class C and every class derived from it, and
Type[Super[C]] accepts the class C and every class that C derives
from. It suits a function that receives a class and relies on every
instance of C being an instance of that class as well, such as a
function that checks whether a container declared to hold instances of the
class it was given can also hold a C. Type[Between[Dog, Animal]]
accepts the classes Dog and Animal and those between them, and refuses
Puppy and object. Inside Hint[...], where the value passed is a type
hint, the ranges hold hints instead. Inside either form, any hint can be a
bound, because the value passed is itself compared as a class or a hint.
The spellings available at each level, including the type argument of a
generic described on the variance
page, are these:
| Where | Spelling | Accepts |
|---|---|---|
| On a value parameter | C |
an instance of C or of a subclass |
| On a value parameter | Exact[C] |
an instance whose type is exactly C |
| On a value parameter | Super[C] |
an instance whose type is C or a class above it |
| On a value parameter | Between[L, U] |
an instance whose type lies between L and U |
On a Type[...] parameter |
Type[C] |
the class C or a subclass of it |
On a Type[...] parameter |
Type[Exact[C]] |
the class C alone |
On a Type[...] parameter |
Type[Super[C]] |
the class C or a class it derives from |
On a Type[...] parameter |
Type[Between[L, U]] |
a class from L up to U, both ends included |
On a Hint[...] parameter |
Hint[X] |
the hint X or a sub-hint of it |
On a Hint[...] parameter |
Hint[Exact[X]] |
the hint X alone |
On a Hint[...] parameter |
Hint[Super[X]] |
the hint X or a hint above it |
On a Hint[...] parameter |
Hint[Between[L, U]] |
a hint from L up to U, both ends included |
| As a type argument | List[Super[C]] |
a list declared to hold C or a type above it |
| As a type argument | List[Between[L, U]] |
a list declared to hold a type from L up to U |
The Type forms combine the same way the value forms do.
Type[Exact[Dog]] is more specific than both Type[Dog] and
Type[Super[Dog]], since the single class Dog belongs to each of them,
so an exact overload wins wherever it applies:
>>> from typing import Type
>>> @dispatch
... def relate(cls: Type[Super[Dog]]) -> str:
... return "Dog or one of its bases"
>>> @dispatch
... def relate(cls: Type[Exact[Dog]]) -> str:
... return "exactly Dog"
>>> @dispatch
... def relate(cls: Type[Puppy]) -> str:
... return "Puppy or a subclass"
>>> relate(Animal)
'Dog or one of its bases'
>>> relate(object)
'Dog or one of its bases'
>>> relate(Dog)
'exactly Dog'
>>> relate(Puppy)
'Puppy or a subclass'
SuperType[C] is a shorter spelling of Type[Super[C]], and
SuperHint[X] is a shorter spelling of Hint[Super[X]]. Each always
needs its bound, since an unbounded lower bound would accept every class or
every hint, which plain type and Hint already express. The alias
expands to the Type form it stands for:
Overlapping bounds
A plain Animal and a Super[Dog] are not ordered against each other,
because each accepts a value the other refuses: a Puppy is an Animal
but its class is not above Dog, and a plain object() has a class above
Dog but is not an Animal. They do share the values whose class lies in
between, a Dog and an Animal, so a call with either of those matches
both overloads and neither is more specific. Such a call raises
AmbiguousMethodError, and
ambiguities() reports the two
overloads as a pair. The same holds for Type[Animal] and
Type[Super[Dog]] with the classes Dog and Animal.
There are two ways to settle the tie. An overload for Exact[Dog] settles
the call with a Dog, since it is more specific than both, but a call with
an Animal still ties. When such a call is made, the ambiguity error
suggests an Exact[...] overload for it:
@dispatch
def feed(x: Animal) -> str: ...
@dispatch # feed(Dog()) and feed(Animal()) are ambiguous
def feed(x: Super[Dog]) -> str: ...
@dispatch # feed(Dog()) now picks this overload; feed(Animal()) still ties
def feed(x: Exact[Dog]) -> str: ...
The alternative is to give one of the two overloads a higher priority
when it is registered, in place of the second overload above. The lower
bound then wins every value the two share, so no call ties and the pair
is not reported:
@dispatch
def feed(x: Animal) -> str: ...
@feed.register(priority=1)
def feed_ancestor(x: Super[Dog]) -> str: ...
A range and a plain class can overlap in the same way. Dog and
Between[Dog, Animal] are not ordered against each other, since a Puppy
belongs only to the first and an Animal only to the second, yet both
accept a Dog. When overloads are registered for both, a call with a Dog
is therefore ambiguous, and the same two remedies apply.
Writing a bound
The lower bound of a range must be a sub-hint of its upper bound. A range with nothing in it is refused as soon as it is written, and when the bounds are merely the wrong way round, the message suggests the reversed spelling:
>>> Between[Animal, Dog]
Traceback (most recent call last):
...
TypeError: Between[Animal, Dog] is empty: Animal is not a sub-hint of Dog, so no hint lies between them. Did you mean Between[Dog, Animal]?
Between[Any, C] is refused as well, because Any is the top of the
order, and no class or hint below C also lies above Any. A range with
no lower bound is written Between[Never, C], which means the same as
plain C. A lower bound cannot be a quoted forward reference, because
nothing would ever resolve it; quoting the whole annotation, as in
'Type[Between[Dog, Animal]]', works as usual.
Written around the whole form, Super[Type[Dog]] means the same as
Type[Super[Dog]] and is normalised to it. Between has no such outer
spelling, so Between[Type[Dog], Type[Animal]] is refused on a value with
a message naming Type[Between[Dog, Animal]]. Exact, Super and
Between cannot be nested inside one another, even through a union or a
TypeVar, since each of them already describes the whole hint at its
position. A bound can be a member of a union on a value parameter, as in
Optional[Super[Dog]], or the bound of a TypeVar used there, but inside
Type[...] or Hint[...] it has to be the whole argument. A bound can
also be the whole type argument of a generic, as in List[Super[int]],
where it widens the argument into a range of types; the
variance page describes that use and
the positions where the generic's variance makes a bound meaningless. A
bound is refused as a constraint of a TypeVar, as an element of a
Tuple, and in the signature of a Callable. Each misplaced bound is
refused when the overload is registered, with an error naming the parameter
and the spelling to use instead.
Type checkers
A static type checker reads Super[C] as Union[C, Any] and
Between[L, U] as Union[L, U, Any], so it never rejects a call that
dispatch itself would accept, whether on a value or inside Type[...].
Inside the function body, though, a checker treats a parameter annotated
Super[Dog] as a Dog, and checks attribute access against Dog,
although the value may be an Animal or a plain object. When that
difference matters, treat such a value as an object in the body. A
parameter annotated Between[Dog, Animal] reads as Dog, Animal or
Any, whose shared attributes are those of Animal, which matches what
the runtime passes. A bound written as a type argument, as in
List[Super[Dog]], reads the same way to a checker, with one limitation for
upper bounds that the variance
page explains together with the spelling that avoids it.