How do type aliases (typing.Tuple, typing.Set, typing.List, etc.) work as an instance but also a class?

4 days ago 2
ARTICLE AD BOX

First of all, these aliases have been deprecated since 3.9. For 3.9+ code, just use tuple[...] instead of using typing.Tuple[...].

These aliases aren't types at runtime. They "work" because well-formed code won't use them in a way that needs them to be types, while static type checkers don't care about what happens at runtime.

>>> assert not isinstance(typing.Tuple, type)

They behave like instance objects since their constructors take arguments

They don't, actually. They don't even have constructors. The constructor to _SpecialGenericAlias takes arguments, as does the constructor to _BaseGenericAlias. But those aren't aliases themselves. They're superclasses of typing.Tuple, typing.List, and the other built-in aliases.

>>> typing.Tuple() # TypeError: Type Tuple cannot be instantiated; use tuple() instead

The pseudo-exception to this is parameterized user-defined generics, which are callable. Even then, however, Spam[object] isn't itself a type; it's an instance of typing._GenericAlias. Calling Spam[object](bar) just forwards the call to Spam.__init__ behind the scenes.

But they also behave like class objects: they satisfy isinstance() of a different class (for example (1,2,3) is a tuple but isinstance((1,2,3), Tuple) is also true

That's because _SpecialGenericAlias overrides __subclasscheck__:

class _SpecialGenericAlias(_NotIterable, _BaseGenericAlias, _root=True): ... def __subclasscheck__(self, cls): if isinstance(cls, _SpecialGenericAlias): return issubclass(cls.__origin__, self.__origin__) if not isinstance(cls, _GenericAlias): return issubclass(cls, self.__origin__) return super().__subclasscheck__(cls) ...

and _BaseGenericAlias overrides __instancecheck__ to use __subclasscheck__:

class _BaseGenericAlias(_Final, _root=True): ... def __instancecheck__(self, obj): return self.__subclasscheck__(type(obj)) ...

This is basically just a hack; it changes the behavior of isinstance to use self.__origin__ (which represents the concrete type corresponding to the alias) instead of self (which is "supposed" to be a class but, for these aliases, isn't). Also, it only works on non-parameterized aliases. isinstance(object(), typing.Tuple[object] will throw an error.

Python type checkers (PyLance) accepts somevar : Tuple as a valid type

That's the whole point of the aliases. They don't do much at runtime; all of their power is in letting static type checkers pretend that they're types and handle static analysis accordingly.

Read Entire Article