Runtime resolution of TypeVar

AFAIK: the current approach to type-check an annotated class would look like this:

from dataclasses import dataclass
from typing import TypeVar, get_args, get_type_hints

T = TypeVar("T")
P = TypeVar("P")

@dataclass
class C[T, P]:
    x: T
    y: P

obj = C[int, str](x=1, y='2')

hints: dict[str, TypeVar] = get_type_hints(obj)  # {'x': T, 'y': P}
args: tuple[type] = get_args(obj.__orig_class__) # (int, str)
params: tuple[TypeVar] = obj.__parameters__      # (T, P)
mapping = dict[TypeVar, type](zip(params, args)) # {T: int, P: str}

# Check each initialized & annotated attributes against their annotations.
for attr, t in hints.items():
    if not hasattr(obj, attr):
        continue # Ignore un-initialized type hints
    value = getattr(obj, attr)
    hint = mapping[hints[attr]]
    # Pretend to perform type check - isinstance() cannot be used like this
    assert isinstance(value, hint)

The above code is abstracted from rttc/type_check/core.py. It is my on-going attempt to implement run-time type checking using parameterized types.

However, this “intricate dance” to match TypeVars against type parameters could have been done by the typing system, resulting in something like this:

# Note that "TypeVar" is converted to "TypeVarResolution".
# _GenericAlias.__call__ should have all necessary information to do it.
hints: dict[str, TypeVarResolution] = get_type_hints(obj)

repr(hints) # {'x': T(~int), 'y': P(~str)}

# Much simpler checking logic
for attr, tvr in hints.items():
    if not hasattr(obj, attr):
        continue
    value = getattr()
    if tvr.resolved:
        hint = tvr.hint
        pass # do whatever with value and hint
    else:
        raise TypeError(f"TypeVar {tvr} not parameterized")

We could do this in CPython, but we don’t have to, since users who want it can write their own libraries outside the core.

My general preference is to try to keep the typing.py runtime as simple as possible. The main use of the standard library typing module is that we can define objects (like typing.TypeVar) that can be used by any type checker and be understood in the same way. This allows users to write typed libraries that work with any type checker. Having these objects in the standard library is important so that they are standardized and always accessible.

Runtime introspection does not have the same needs. Users who want it could write and publish their own libraries, but there isn’t the same need for standardization. Instead, the standard library aims to provide only the basic building blocks to introspect types at runtime, such as typing.get_args. Keeping the standard library simple makes my job as a CPython maintainer easier, and enables the runtime type checkers to innovate faster as they are not tied to CPython’s release cycle.

It hasn’t been a long time since I started digging into the typing system, so please forgive me if these statements are not accurate:

During the implementation of the library I mentioned above, I noticed these problems:

  1. __orig_class__ and __parameters__ are typing module’s internal protocol. Their names and contents are subject to change across versions.
  2. Their availability are not guaranteed. For example _GenericAliasBase.__call__ will give up if setting attribute __orig_class__ triggers any exception.

This could impose a lot of burdens for a 3rd party type checker to keep up with the stdlib. They might also suffer from inconsistent behaviors due to unpredictable availability of information.

With that said, I believe going this one step further could provide a more reliable and easy-to-use interface for whoever that wants to do run-time type checking.

Coming back with a potentially useful idea: type-hinted class can dynamically resolve type variables according to it’s arguments and member variables.

@dynamic_type
class C[T]:
    x: T

c = C()

# Pretending __orig_class__ is available:

c.x = 1
c.__orig_class__ # __main__.C[T(~int)]

c.x = "magic"
c.__orig_class__ # __main__.C[T(~str)]

C(x=1).__orig_class__ == C[int] # True
C(x="magic").__orig_class__ == C[str] # True