bagof.dispatchers.core
Comparing type hints to each other, and values to type hints.
bagof.dispatchers.core depends on nothing beyond typing_extensions,
which is what lets the rest of the bagof family sit on top of it
without pulling in the dispatch machinery above it. Its central exports
are issubhint, which decides whether one hint describes a set of
values narrower than another hint's, and ishintstance, which
decides whether a given value falls inside the set a hint describes.
Both are assembled from the smaller introspection functions exported
alongside them, which normalise a hint, take apart a generic alias, or
recognise a TypedDict. The dispatch engine defined at the top of this
package is one consumer of this layer, built with no assumption that it
is the only one.
Attributes
UNION_TYPES
module-attribute
Every object that can appear as the origin of a union hint.
On a Python old enough that the X | Y syntax does not exist, this
holds only Union itself; from 3.10 on, where X | Y produces its own
UnionType object distinct from Union, it holds both.
UNSET
module-attribute
UNSET = Unset()
Marks a parameter that a caller left out of a call entirely.
Note
UNSET means the argument was never given. It is a separate concept
from None, which a caller can pass deliberately as a value in
its own right.
Classes
UnknownHintWarning
Bases: RuntimeWarning
Raised when a hint the subtype relation cannot identify is met.
A hint that this package does not recognise, for instance a
construct added by a Python release newer than the one this package
was written against, is not rejected outright. It is instead treated
as equivalent to Any: every other hint is accepted as
broader than it, and it is narrower than nothing except itself and
Any. A method annotated with such a hint therefore stays reachable
by dispatch rather than becoming permanently unmatchable, and this
warning is raised once for each distinct construct met this way, so
that the fallback does not pass unnoticed.
Unset
A class restricted to a single instance, for building markers with.
Every subclass gets an instance of its own the first time it is constructed, rather than reusing an ancestor's, so several distinct markers can each be written as a one-line subclass without extra bookkeeping.
Functions:
ishint
Report whether x is a type hint, rather than an ordinary value.
A type hint is anything that can legitimately stand as an annotation
or as an argument to the subtype relation, which includes far more
than a class alone. A bare None counts, standing for
NoneType, as does a string or a
ForwardRef naming a type not yet resolved. A
class counts, and so does a parametrised generic such as
List[int], which carries a typing origin. A recognised
special form such as Union or
Literal counts too, and indeed any object defined
in typing or typing_extensions, which leaves room for a
construct those modules add in a future release. Finally, the
TypedDict marker, a NewType, and
a PEP 695 type X = ... alias each count.
An everyday value that was never meant to be a hint, such as a number, a container instance, an ordinary function, or a sentinel object, fails this check. A bare string is the one deliberate exception: it counts as a hint here, since it may be a forward reference, even though most callers that reject non-hints reject a bare string too.
eq_safenan
Map a value to a form that compares equal to any other NaN.
Comparing values that might be NaN with plain == is unsafe, since
float("nan") != float("nan") even for the very same
object. Applying this function to both operands before comparing
them sidesteps that: every real NaN maps onto one shared sentinel,
so two NaNs end up comparing equal, while every other value passes
through unchanged.
Note
A complex number carrying a NaN in either its real or its
imaginary part is normalised component by component, so a
NaN-bearing complex compares equal to itself, and two complex
values compare equal when their non-NaN parts match and NaN
falls in the same position. A Decimal
carrying a NaN, whether quiet or signaling, is recognised too and
maps onto the same marker as a real NaN. A numpy scalar is
recognised through its registration under the numbers.Real
ABC, so this needs no numpy import of its own.
Example
>>> nan = float("nan")
>>> nan == nan
False
>>> eq_safenan(nan) == eq_safenan(nan)
True
>>> cnan = complex(float("nan"), 2.0)
>>> cnan == cnan
False
>>> eq_safenan(cnan) == eq_safenan(cnan)
True
>>> from decimal import Decimal
>>> Decimal("nan") == Decimal("nan")
False
>>> eq_safenan(Decimal("nan")) == eq_safenan(Decimal("nan"))
True
get_args_uw
Find a hint's type arguments, unwrapping Annotated first.
An empty tuple comes back when the hint is not a generic type.
get_concrete_type
Find a concrete, instantiable type standing in for a type hint.
Any Annotated wrapper is removed from the hint
first. If what remains has an origin, that origin is used. If it is
instead a TypeVar, the first of the following
that applies is used: its default, the first of its constraints, its
bound, or the fallback type when one was given. Failing all of
those, a TypeError is raised.
Whatever is arrived at this way is returned as is when it turns out
to be concrete and non-abstract. Anything else falls back to
fallback, when one was given, or otherwise raises a
TypeError.
Note
A constrained type variable has no single concrete type of its own, since it stands for the union of its constraints, so its first constraint is taken as a stand-in for it.
get_origin_uw
Find a hint's origin, unwrapping Annotated first.
The hint itself is returned, rather than None, when it is not a
generic type.
is_typeddict
Report whether cls is a TypedDict or a subclass.
Tip
This differs from typing.is_typeddict,
which returns False for TypedDict
itself; here, the bare marker itself counts too.
issubclassable
issubscriptable
Report whether x can be subscripted, written as x[...].
This comes back True when x is a type defining
__class_getitem__, or an instance defining __getitem__, and
False for everything else.
mro_index
Find where hint's class sits in value_type's MRO, or None.
This is what drives the MRO tie-break described in RFC 0001 ยง2.2:
when two hints are otherwise incomparable, the one whose class names
a more derived base of value_type wins, so a diamond such as
D(B, C) resolves to B, exactly as
functools.singledispatch does. Value
dispatch checks this against an argument's runtime type, while
resolve_hint checks it
against a class query to break a tie between equally specific class
keys.
A refinement is only defined when the hint names a single ordinary
class that is a nominal base of value_type. An
Exact[C] hint is treated as C
itself. A bare class returns its own index in
value_type.__mro__. A bare, unparametrised alias is
treated as its origin class regardless of which spelling it was
written in, so List and list name the same
position, as do Sequence and
collections.abc.Sequence. A class that does not appear in
the MRO at all, such as a Protocol or an ABC satisfied
structurally or through registration rather than inheritance, gives
no refinement and returns None. The same is true of a
Union, a Literal, a type[...], or
any other parametrised generic, and of a
Super or a
Between bound, which names a range of
classes rather than one position in the MRO.
Example
Returns:
| Type | Description |
|---|---|
int or None
|
The index of the hint's class in |
normalise_hint
Put a hint into its canonical form.
A bare None is understood as a hint to mean
NoneType, and is replaced by it. A PEP 695
type X = ... alias is resolved to the hint it stands for, a
NewType is resolved to its supertype, and the
transparent qualifiers Required,
NotRequired, ReadOnly,
Final, and ClassVar are
unwrapped down to the hint each one wraps. A bare, unsubscripted
Optional is read as a bare
Union, since both stand for "some union". An
Exact or a
Super wrapping a whole
Type or Hint is rewritten
to carry the marker on the inner type instead, so that
Exact[Type[int]] becomes Type[Exact[int]] and
Super[Hint[int]] becomes Hint[Super[int]]. A
Between has no such outer spelling, so
one written around a whole Type or Hint, such as
Between[Type[A], Type[B]], is left as it is and refused
with a message naming Type[Between[A, B]].
These steps repeat until the hint stops changing, so an alias that
expands into a qualified NewType is resolved all the way through
rather than only one layer deep. Any hint none of these steps
touches comes back unchanged.
Note
Only a bare None is replaced this way. A None inside a
hint keeps its own meaning: Literal[None] describes
the literal None value, not a type. Annotated
is not stripped here, because its metadata can carry an
exactness marker that the relation reads.
resolve_alias
Resolve a PEP 695 type X = ... alias to the hint it stands for.
A bare alias resolves to its stored value directly. A subscripted
generic alias, such as L[int] for
type L[T] = list[T], has its type arguments substituted in
first. An alias standing for another alias is followed all the way
through to the end, and a reference cycle stops resolution rather
than recursing without limit. A hint that is not an alias at all
comes back unchanged.
resolve_newtype
Resolve a NewType to its supertype, recursively.
A NewType gives an existing type a distinct name, and dispatch
treats it exactly as that underlying type, so this follows the chain
down to it. A hint that is not a NewType at all comes back
unchanged.
safe_get_args
Find a hint's type arguments, without raising on a plain type.
An empty tuple comes back when the hint is not a generic type.
Passing unwrap strips a wrapper such as
Annotated off the hint first, before its
arguments are read.
safe_get_origin
Find a hint's origin, without raising when the hint has none.
Passing unwrap strips a wrapper such as
Annotated off the hint first, before its
origin is read.
Note
Unlike typing.get_origin, this returns the hint itself,
rather than None, when the hint is not a generic type.
safe_isinstance
Report whether obj is an instance of cls, without ever raising.
Warning
A TypedDict cannot be instance-checked at
all: Python refuses isinstance(value, SomeTypedDict)
outright, and a TypedDict leaves no trace on the dict it
describes for anything to recognise at runtime. This function
therefore answers False for one; check the shape of the
dict directly instead.
safe_issubclass
type2hint
Convert a type to a subscriptable type hint standing in for it.
When x has no __class_getitem__ of its own, this looks up the
type hint that corresponds to it; on Python 3.8, for instance,
type2hint(list) returns typing.List.
Anything else comes back unchanged.
typeddict_field_hints
cached
Map each declared field of a TypedDict to its hint.
The mapping covers every key the class declares, both its own and
those inherited from TypedDict bases, each paired
with the hint written for it. Any Required or
NotRequired qualifier is left in place on the
hint; reading it off is typeddict_required_keys's job instead,
and a caller who only wants the value type can simply let the
relation look straight through the qualifier.
A string annotation is resolved against the class's own module where
possible, using typing.get_type_hints. When a
forward reference cannot be resolved, the raw name is kept for that
one field alone, so the caller sees the name rather than nothing at
all, while every sibling field that can be resolved still is.
typeddict_required_keys
cached
Find the required keys of a TypedDict.
When the class carries __required_keys__, that attribute is read
directly, since it is the only source that correctly accounts for
Required and NotRequired
(whether or not nested inside Annotated) and
for inheriting from bases declared with a different total=.
When the class has no __required_keys__ at all, this falls back to
reading __total__ instead: typing.TypedDict only gained
__required_keys__ in Python 3.9, and before that a key's
requiredness came from the class's own total= alone, since
per-key Required and NotRequired did not yet exist.
Warning
On older Pythons, a typing.TypedDict that mixes total=
settings across its bases can only be read through the
subclass's own total=, which is not correct for every key.
A subclass has no __orig_bases__, and its __mro__ reaches
only dict, so the standard library records neither which
class declared a given key nor a usable link back to that base,
leaving the per-key answer simply unrecoverable.
This error can go in either direction. A total=True key
inherited into a total=False subclass is reported as optional,
so a value missing it is wrongly accepted, as when {} is
matched against a shape that actually requires that key. A
total=False key inherited into a total=True subclass is
reported as required, so an otherwise valid value is wrongly
rejected. Use typing_extensions.TypedDict,
which reimplements the class precisely and records
__required_keys__ correctly on every version, whenever this
distinction matters.
unwrap
Strip a hint down to its argument, when its origin is one of origin.
When TypeVar is listed among the origins to
unwrap, a type variable is replaced by whichever of its default, the
union of its constraints, or its bound comes first and actually
exists, tried in that order.
resolve_hint
resolve_hint(hint: Any, mapping: Mapping[Any, Any], *, default: Any = UNSET, ambiguity: str = 'raise') -> Any
Return the value stored under the most specific key that accepts hint.
A key K accepts hint when issubhint(hint, K)
holds, meaning the query is a sub-hint of the key and so every value
the query describes is also described by K. The best key among
those that accept the query is the most specific one, the accepting
key that is itself a sub-hint of every other accepting key. A key
equal to the query wins outright regardless of specificity, and an
Exact[C] key is reachable this way
too, by a query equivalent to C itself.
When several keys are equally specific and the query is a class, the
tie is broken using the query's MRO, following RFC 0001 ยง2.2 step 3:
whichever key's class sits nearest to the query in that MRO wins.
{Enum, str} therefore resolves
class Color(str, Enum) to str, and a diamond
D(B, C) resolves {B, C} to B, in both cases
independently of the order the keys happened to be registered in. A
tie the MRO cannot settle, because the query is not a class or
because the competing keys sit at the same distance in it, is handed
to the ambiguity parameter.
Example
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hint
|
Any
|
The query hint. A bare |
required |
mapping
|
Mapping[Any, Any]
|
The registry to search, keyed by type hint; its values can be anything the caller chose to store. |
required |
default
|
Any
|
The value to return when no key accepts the query. Left at its
default of |
UNSET
|
ambiguity
|
str
|
What to do when two equally specific keys both accept the query
and neither the query nor the mapping breaks the tie.
|
'raise'
|
Returns:
| Type | Description |
|---|---|
Any
|
The stored value, or |
ishintstance
Report whether obj belongs to the set of values hint describes.
ishintstance extends isinstance to the full vocabulary of
type hints rather than only classes, so its second argument can be
a union, a literal, a generic, a protocol, or any other construct
this package understands. What counts as membership then depends on
the specific kind of hint given. The value obj may be anything at
all, but hint has to be a genuine type hint: like isinstance
against a second argument that is not a class, ishintstance raises a
TypeError when hint is not one.
A type or Type[...] hint requires obj to be a
class itself, and a valid subclass of whatever the hint names. When
the hint is Type[Exact[C]], obj must be exactly the class
C, not a subclass of it, and when the hint is
Type[Super[C]], obj must be C or a class that C
derives from. When the hint is Type[Between[L, U]], obj
must be a class that lies between L and U, both included. A
Hint[X]
hint requires obj to itself be a type hint that is a sub-hint of X,
so that a hint passed as a value can be dispatched on;
Hint[Exact[X]] narrows that to the exact hint X, and
Hint[Super[X]] turns it around, accepting X and every
hint that X is a sub-hint of, while Hint[Between[L, U]]
accepts the hints between L and U, both included. A
Literal hint requires obj to equal one of its
listed values, with the type checked alongside the value, so
True does not satisfy Literal[1] even though
True == 1 in Python. A Union hint is
satisfied whenever obj satisfies any one of its members.
An Exact,
Super or
Between hint standing on its own
compares the class of obj against a range of classes, the same way
Type[...] of that hint compares a class passed in.
Exact[C] requires the class of obj to be C itself,
Super[C] requires it to be C or a class that C derives
from, and Between[L, U] requires it to lie between L and
U. Each bound must then be a hint that a class can be compared
against, and one that reads the value itself, such as a Literal,
raises a TypeError.
When obj is itself a hint passed as a value to a Hint[X]
hint, only a bound at the top level of obj is checked in this way,
so ishintstance(Super[Literal[1]], Hint[Any]) raises the
same error. A misplaced bound nested inside obj, as in
Optional[Super[Literal[1]]] or
Sequence[Super[int]], is reported only when the
comparison with X reaches it, and a comparison with
Any never does, because every hint is a sub-hint of
Any.
A TypedDict hint describes the shape of a mapping
rather than its class: obj must be a dict carrying every
required key, with each key it does carry holding a value of that
field's declared type. Keys beyond the ones the TypedDict declares
are handled according to that TypedDict's own policy, since an
ordinary open one allows them, a closed=True one rejects them, and
one declared with extra_items= checks each of them against that
type. A field that is itself a nested TypedDict or container is
checked the same way, recursively.
A runtime_checkable protocol that
declares data members, such as one with name: str, is
checked structurally, in the same spirit as isinstance.
obj's class either names the protocol among its own bases, or
every declared data member is present on obj, whether set directly
on the instance or supplied by the class through an annotation, a
class attribute, a property, or a slot, never through a
ClassVar; every method, in either case, must also be
defined somewhere on the class. An annotated member counts as present
even on an
instance that never actually set it, a case where isinstance
itself would find nothing, because that mirrors how a type checker
reads the same annotation and keeps this check aligned with
issubhint. A member the protocol declares as a
ClassVar is read only from the class, and only a
ClassVar annotation is treated that way. None of this
lookup runs the value's own code: a property is never called, and
__getattr__ is never invoked. A protocol declaring only methods,
with no data members at all, is instead checked directly against the
value's type.
A parametrised generic, such as List[int] or
Box[int], first requires obj to be an instance of its
class. It only goes on to check the type arguments when obj
actually declares them, which happens for an instance built as
Box[int]() or for one of a class written against a
parametrised base such as class IntList(List[int]). Those
declared arguments are then compared the same way
issubhint compares them, so an invariant position demands
exactly the same type. Box[int]() does not satisfy
Box[object], and IntList does not satisfy
List[object], even though it does satisfy
List[Any], plain list, and
Sequence[object]. A bound written as a type argument is
compared with the declared argument in the same way, so an
IntList satisfies List[Super[int]], whose
argument covers int and every type above it. The two uses
of a bound ask different questions: Super[int] on its own
asks about the class of obj, while List[Super[int]] asks
about the argument that obj declares.
Every other hint falls back to issubhint(type(obj), hint).
Note
Box[int]() only writes its record onto the instance
once __init__ returns, so a call dispatched on self from
inside __init__ still sees a value that declares nothing yet.
An instance that has no way to hold that record at all, such as
one of a class with __slots__ and no __dict__, or a frozen
dataclass, never declares its arguments either.
Warning
A container's item types are never checked here. A plain
[1, 2] declares no type arguments of its own, so this
function treats it as a valid List[str]; Python
itself refuses isinstance(x, list[int]) for the same
reason. Checking the items would mean iterating them, which is
left to the caller to decide on; bagof.validators is what does
it.
issubhint
Report whether hint is a sub-hint of superhint.
One hint is a sub-hint of another exactly when every value the first
describes is also described by the second, which makes issubhint
the type-level counterpart of asking whether every instance of one
class is also an instance of another.
Both arguments must be type hints. issubhint mimics
issubclass, so a value that is not a hint raises a
TypeError rather than being read as a catch-all, and the error
names the offending argument, the left one first when both are at
fault. A bare string is accepted on the left, where, being an
unresolved forward reference rather than a class, it is a sub-hint of
nothing and simply compares False. On the right it is rejected,
since it cannot be resolved against any namespace to compare against.
Where a hint is generic, each of its argument positions is compared
according to the variance that generic declares for it, following
PEP 484. A covariant position, such as a Sequence's
element type, carries the relation forward unchanged, so
Sequence[bool] is a sub-hint of
Sequence[int]. A contravariant position reverses the
relation instead. An invariant position, such as a mutable container
like list, or an unflagged user TypeVar,
demands that the two arguments describe exactly the same values, so
List[bool] is not a sub-hint of List[int]
even though bool is a subtype of int. A free TypeVar
or Any standing in the super-hint's position still acts
as a top that an invariant position may widen to. A hint given with
no arguments at all is never a sub-hint of one that has them, since a
bare list might hold anything and so cannot stand in for
the narrower List[int].
Note
An unparametrised Union or Literal asks a
different question when it appears as the super-hint: not
"does every value here belong to some member", but "is this
very hint one of those forms". issubhint(int, Union)
is therefore False, since a plain int is
not itself a union, even though
issubhint(int, Union[int, str]) is True.
This is what lets either form be used as a BOUND, and
it is also why the relation is not transitive through a bare
Union.
Note
A class counts as a sub-hint of a
runtime_checkable protocol
declaring data members, such as one with name: str,
whenever it declares those members itself, read much as a type
checker would read it. Either the class names the protocol among
its own bases, or it declares each member as that member's own
kind. An ordinary member is declared through an annotation such
as name: str (including a dataclass field), or
through a class attribute, a property, or a slot, but never
through a ClassVar. A member the protocol itself
declares as ClassVar can only be declared back
through a matching ClassVar annotation. An annotation
counts whether or not the attribute is ever actually assigned,
and ishintstance counts it the same way on every instance,
to keep the two checks aligned. Unlike a type checker, a
read-only member, such as a property with no setter or one
typed Final, still counts as satisfying an ordinary
member here.
A bare, unsubscripted Annotated super-hint is
read structurally: only an Annotated form is a sub-hint of
it, so issubhint(int, Annotated) is False. A
Hint form is ordered only against another
Hint form, never against an ordinary type, since its values
are hints rather than values of a type. Inside a
Type or Hint argument,
Exact behaves as the same leaf it is on
its own, so Type[Exact[int]] sits below
Type[int] and nothing ordinary sits below it.
A Super bound is ordered in the opposite
direction to the class it names. On a value,
Super[numbers.Integral] is a sub-hint of
Super[int], because the classes above
numbers.Integral are among the classes above
int, and the same holds inside Type or
Hint. Exact[C] sits below both C and
Super[C], while C and Super[C] are not
ordered against each other in either direction.
Between[L, U] names both ends of
such a range, and one range is below another when it lies inside
it, so Between[D, C], for a class D derived from C, is
a sub-hint of both C and Super[D]. On a value, each
bound of a Super or a Between must be a hint that a class can be
compared against, and one that reads the value itself, such as a
Literal, raises a TypeError. A bound also raises one
in a position where it cannot be read: inside another bound, as a
constraint of a TypeVar, as a union member or a
TypeVar bound inside the argument of Type, of
Hint or of a generic, as an element of a
Tuple, and in the signature of a Callable.
A bound can also be the whole type argument of a generic. At an
invariant position it names a range of arguments rather than a
single one, and the parametrisation stands for every parametrisation
whose argument lies in that range. List[int] and
List[numbers.Integral] are therefore sub-hints of
List[Super[int]], and List[bool] is not. One
bounded argument is below another when its range lies inside the
other's, a plain argument counts as a range holding only itself, and
a TypeVar bounded by B counts as the range
Between[Never, B]. Each bound names its own range, so
List[List[int]] is not a sub-hint of
List[List[Super[int]]], whose outer argument is the single
hint List[Super[int]]. At a covariant or a contravariant
position the variance already widens the argument. A bound that says
no more than the variance does, such as the one in
Sequence[Between[Never, int]], is read as the plain
argument, and one with an end that the variance would ignore, such as
the one in Sequence[Super[int]], raises a TypeError.
Example
>>> from typing import List, Sequence, Union
>>> issubhint(bool, int)
True
>>> issubhint(Union[int, str], Union[int, str, bytes])
True
>>> issubhint(Sequence[bool], Sequence[int]) # Sequence covariant
True
>>> issubhint(List[bool], List[int]) # list invariant
False
>>> issubhint(list, List[int]) # a bare list may hold anything
False
>>> issubhint(int, str)
False