I’d like to propose a new API for accessing type parameter values at runtime using for classes using PEP 695 generic syntax. I’m not sure if it’s a bit too early to be proposing these because it requires PEP 718 (subscripting functions at runtime) to be accepted and optionally PEP 696 (type parameter defaults) but I think it’s still an interesting idea either way.
The full list of things I’d like to do:
-
Adding an
__args__
attribute that returns the specialised parameters to the class -
Adding any type parameters to be available directly on the class as overwrite-able instance variables
-
Substitution of default type parameters at runtime (assuming PEP 696 is accepted)
-
Automatically adding
__orig_class__
to a class’s slots if it’s subscriptable (even if it’s defined in C)
The following change to TypeVarLike
s:
- Adding
__value__
as a way to compute the specialised value of a type parameter after subscription (if it has non-default parameters)
The following change to GenericAlias
es:
- Hooking
__getattr__
to handle accessing__args__
by name on the instance.
Motivation
Currently getting the specialised types for Generic
types is unintuitive and unreliable
class Foo[T]: ...
Foo[int]() # How do I get `int` inside Foo?
>>> Foo[int]().__orig_class__.__args__
(int,)
This however doesn’t work inside __new__
/__init__
or any methods called from them as GenericAlias.__call__(*args, **kwargs)
only sets __orig_class__
after self.__origin__(*args, **kwargs)
returns.
class Bar[T]:
def __init__(self):
self.__orig_class__
>>> Bar[int]() # AttributeError: Bar has no attribute __orig_class__
Now what about if I subclass a generic?
class Bar(Foo[str]): ... # how do I now get `str`?
>>> types.get_original_bases(Bar)[0].__args__
(str,)
And what about a type parameter inside a generic function?
def foo[T](): ...
>>> foo[int]()
This isn’t even possible without using implementation details/frame hacks.
With the new roots of runtime type checking beginning to sprout, I think it’s unacceptable to have this kind of hard-to-use interface which is full of edge cases.
e.g.
class Slotted[T]:
__slots__ = ()
Slotted[int]().__orig_class__ # AttributeError: 'Slotted' object has no attribute '__orig_class__'
I propose a new interface design which solves all of the above problems by being easy to use and much more reliable:
>>> Foo[int]().__args__
(int,)
>>> Foo[int]().T.__value__
int
>>> Bar.__args__
(str,)
>>> Bar.T.__value__
str
def foo[T]():
return T.__value__
>>> foo[bool]()
bool
Anecdotally I’ve seen many requests for such a feature and I’ve needed it multiple times when writing typed code to get type parameters without duplicating values throughout code.
Prior discussion:
- https://github.com/python/typing/issues/629
- https://mail.python.org/archives/list/typing-sig@python.org/thread/T7VEN5HYHIT5ABNJHYOW434JHELTTKT3/
I can send a more complete draft of this once I have a better idea of how to implement this in cpython.
Thanks!