pythongenericspython-typingisinstance

Why does Python not allow Generics with isinstance checks?


Running the below code with Python 3.12.4:

from typing import Generic, TypeVar

T = TypeVar("T")

class Foo(Generic[T]):
    def some_method(self) -> T:
        pass

isinstance(Foo[int](), Foo[int])

It will throw a TypeError: Subscripted generics cannot be used with class and instance checks.

Traceback (most recent call last):
  File "/path/to/a.py", line 9, in <module>
    isinstance(Foo[int](), Foo[int])
  File "/path/to/.pyenv/versions/3.12.4/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/typing.py", line 1213, in __instancecheck__
    return self.__subclasscheck__(type(obj))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/path/to/.pyenv/versions/3.12.4/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/typing.py", line 1216, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

What was the rationale for Python not allowing isinstance checks with Generics?


Solution

  • At runtime, type arguments are not verifiable. Such relationships are only checked at type-checking time:

    Note that the runtime type (class) of [the variables given as example] is still just Node ā€“ Node[int] and Node[str] are distinguishable class objects, but the runtime class of the objects created by instantiating them doesn't record the distinction. This behavior is called "type erasure"; it is common practice in languages with generics (e.g. Java, TypeScript).

    ā€” Generics - Specification for the Python type system

    For example, if isinstance(..., list[str]) were to be treated at isintansce(..., list), the result would be very confusing:

    >>> a = [1, 2, 3]
    >>> isinstance(a, list[str])
    True
    

    Also consider the case where type arguments are not concrete:

    def is_list_of(v: Any, element_type: type[T]) -> TypeIs[list[T]]:
        return isinstance(v, list[T])  # ???
    

    This has always been the case since PEP 484, albeit not very explicitly (Callable and Generics are two different sections, and this note seems to mean something slightly different than what we are taking it to be, though conceptually the same):

    [...] isinstance(x, typing.Callable[...]) is not supported.

    However, there are runtime type checkers that do this kind of heavy-lifting, such as Pydantic.

    Historically, the PEP 484 note was added in this commit, and type-erasure-related problems were discussed in this GitHub issue. It seems that GvR and other contributors were initially opposed to Node[int]() (which then worked at runtime but very slow, and was forbidden by Mypy) due to performance problems.