Skip to content

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

UNION_TYPES = (tx.Union,) if UnionType is tx.Union else (tx.Union, UnionType)

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

ishint(x: Any) -> bool

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.

Example

>>> ishint(int)
True
>>> ishint(tx.Union)
True
>>> ishint("Foo")
True
>>> ishint(1)
False

eq_safenan

eq_safenan(x: Any) -> Any

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

get_args_uw(hint: Any) -> tuple[Any, ...]

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

get_concrete_type(hint: Any, fallback: type = UNSET) -> Type[Any]

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.

Example

>>> get_concrete_type(List[int])
<class 'list'>
>>> get_concrete_type(TypeVar("T", int, str))
<class 'int'>

get_origin_uw

get_origin_uw(hint: Any) -> Any

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

is_typeddict(cls: Any) -> bool

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

issubclassable(cls: Any) -> bool

Report whether cls is a type, or is TypedDict.

Tip

This differs from isinstance(cls, type) in that it answers True for TypedDict and its subclasses too, even though none of them is technically a type.

Note

A typing construct such as Any, Union, or Literal is never subclassable, whichever Python version is running. Some of these become classes on recent Pythons, Any from 3.11 and Union from 3.14, so isinstance(hint, type) alone would answer differently across the versions this package supports.

issubscriptable

issubscriptable(x: Any) -> bool

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

mro_index(hint: Any, value_type: type) -> int | None

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

>>> class B: pass
>>> class C: pass
>>> class D(B, C): pass
>>> mro_index(B, D) < mro_index(C, D)   # D resolves to B
True

Returns:

Type Description
int or None

The index of the hint's class in value_type.__mro__, where 0 is value_type itself, or None when no refinement applies.

normalise_hint

normalise_hint(hint: Any) -> Any

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.

Example

>>> normalise_hint(None)
<class 'NoneType'>
>>> normalise_hint(int)
<class 'int'>

resolve_alias

resolve_alias(hint: Any) -> Any

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_newtype(hint: Any) -> Any

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

safe_get_args(hint: Any, unwrap: Any = ()) -> tuple[Any, ...]

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

safe_get_origin(hint: Any, unwrap: Any = ()) -> Any

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

safe_isinstance(obj: Any, cls: Any) -> bool

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.

Example

>>> safe_isinstance(1, int)
True
>>> safe_isinstance(1, (str, int))  # a tuple, like `isinstance`
True
>>> safe_isinstance(1, "not a type")  # no error
False

safe_issubclass

safe_issubclass(subcls: Any, cls: Any) -> bool

Report whether subcls is a subclass of cls, without ever raising.

Warning

When cls is a TypedDict, this looks at subcls's __orig_bases__ rather than its __bases__. A plain dict is not treated as a subclass of a TypedDict here; the relation only holds the other way round.

Example

>>> safe_issubclass(bool, int)
True
>>> safe_issubclass(bool, (str, int))  # a tuple, like `issubclass`
True
>>> safe_issubclass(int, "not a type")  # no error
False

type2hint

type2hint(x: Any) -> Any

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.

Example

>>> type2hint(frozenset)
typing.FrozenSet
>>> type2hint(3)  # not a type, so unchanged
3

typeddict_field_hints cached

typeddict_field_hints(cls: Any) -> dict[str, Any]

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.

Example

>>> class Movie(TypedDict):
...     title: str
...     year: NotRequired[int]
>>> sorted(typeddict_field_hints(Movie))
['title', 'year']

typeddict_required_keys cached

typeddict_required_keys(cls: Any) -> FrozenSet[str]

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.

Example

>>> class Movie(TypedDict):
...     title: str
...     year: NotRequired[int]
>>> typeddict_required_keys(Movie)
frozenset({'title'})

unwrap

unwrap(hint: Any, origin: Any = (Annotated,)) -> Any

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.

Example

>>> from typing import Annotated
>>> unwrap(Annotated[int, "meta"])
<class 'int'>
>>> unwrap(Annotated[Annotated[str, 1], 2])
<class 'str'>
>>> unwrap(int)  # unchanged
<class 'int'>

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

>>> registry = {int: "number", object: "any"}
>>> resolve_hint(bool, registry)
'number'
>>> resolve_hint(str, registry)
'any'
>>> resolve_hint(list, {int: "n"}, default="?")
'?'

Parameters:

Name Type Description Default
hint Any

The query hint. A bare None is treated as NoneType, as it is everywhere else in the relation.

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, such a query raises NoMethodError instead.

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", the default, raises AmbiguousMethodError. "warn" instead picks whichever tied key comes first in the mapping's own order and emits a RuntimeWarning. "ignore" makes that same choice without a warning.

'raise'

Returns:

Type Description
Any

The stored value, or default when nothing accepts the query.

ishintstance

ishintstance(obj: Any, hint: Any) -> bool

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.

Example

>>> ishintstance(1, int)
True
>>> ishintstance(1, Literal[1, 2])
True
>>> ishintstance(bool, Type[int])
True

issubhint

issubhint(hint: Any, superhint: Any) -> bool

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