1 of 17

TypeVarTuple and typing.py

Pradeep Kumar Srinivasan

11th January 2021

2 of 17

What we'd need to do

  • Syntax change to allow *Ts
  • Arity checks for generic classes
  • Add TypeVarTuple and Unpack
  • Allow Tensors of arbitrary ranks

3 of 17

What we're aiming for

Generic should be able to accept

  • class Tensor(Generic[T1, *Ts, T2]): …
  • class Tensor(Generic[T1, Unpack[Ts], T2]): …

Tensor should be able to accept arbitrary parameters:

  • Tensor[int, str, bool, str]
  • Tensor[int, str]

4 of 17

Syntax change: *Ts

  • Q: Just to confirm, does PEP 637 allow multiple `*`?
    • Foo[int, *Ts, str, *Ts2]
  • Q: Does it allow a positional argument after a `*`?
    • class Foo(Generic[T, *Ts, T2])

5 of 17

Arity check

GenericMeta is a metaclass that checks that generic classes are provided the right number and types of arguments

  • TypeError: Too many parameters for typing.List; actual 2, expected 1

Tensor will need to accept an arbitrary number of arguments

  • Tensor[L[480], L[360], L[640]]
  • Tensor[L[480]]
  • Tensor[()]

(where L = Literal)

6 of 17

__parameters__

  • Introduced in PEP 585 (Type Hinting Generics In Standard Collections):

# * __parameters__ is a tuple of unique free type parameters of a generic

# type, for example, Dict[T, T].__parameters__ == (T,)

  • Mainly used within typing.py to check arity of generic class instantiations

Consider:

Ts = TypeVarTuple('Ts')

class Foo(Generic[*Ts]): ...

foo: Foo[int, str] = Foo()

class Bar(Generic[Ts]): ...

bar: Bar[Tuple[int, str]] = Bar()

We need to distinguish Ts and *Ts / Unpack[Ts]

7 of 17

Should TypeVarTuple subclass TypeVar?

Pro subclassing:

  • Supports isinstance(foo, TypeVar)�(typing.py, typing_extensions.py, e.g. MonkeyType)
  • They both have an arity of 1

Con subclassing:

  • Different semantics
  • No shared attributes/arguments
    • At least, yet
    • In the future: bounds, variance, constraints

8 of 17

Unpack[Ts]

  • class Foo(Generic[T, *Ts]): …
  • Foo.__parameters__ == (T, Unpack[Ts])

  • Arity check must check for the appropriate number of parameters
    • *Ts matches 0+ parameters
    • Foo[int, str, bool] # valid
    • Foo[int] # valid
    • Foo[()] # error
  • Note: Unpack can be used on a Tuple as well
    • Foo[int, *Tuple[str, bool]]
    • Foo[int, *Tuple[str, *Ts]]

9 of 17

What if Tensor is given no parameters?

  • List without parameters is treated as List[Any]
  • class Tensor(Generic[*Ts]): ...
  • How to treat Tensor?

  • Option 1: Treat it as Tensor[Any]
    • That makes it a Tensor of rank 1
  • Option 2: Treat it as equivalent to Tensor[()]
    • Confusing
  • Option 3: Treat it as a Tensor of arbitrary rank

10 of 17

Tensors of arbitrary rank

  • Basically, allow Tensor[Any, …]
  • Ts would be bound to Tuple[Any, …]
  • Example:

def complicated_function(t: Tensor[Any, …]) -> int: …

my_tensor: Tensor[L[480], L[360]]

complicated_function(my_tensor)

  • Con: This introduces unsoundness
    • Just like Tuple[Any, …]
  • Pro: This could aid gradual typing
    • Work out of the box for existing types like def foo(t: Tensor) -> None: ...

11 of 17

Other things

Anything else?

class _TypeAlias(_TypingBase, _root=True):

"""Internal helper class for defining generic variants of concrete types."""

12 of 17

Thank you!

13 of 17

Extra slides

14 of 17

Unexpanded Ts

  • class Foo(Generic[T1, Ts1, Ts2]): …
  • This is useful for classes generic in multiple TypeVarTuple
  • This should accept Foo[int, Tuple[int, str], Tuple[bool]]
  • Unexpanded Ts matches exactly 1 parameter, just like T
  • No major changes needed here

15 of 17

# * __parameters__ is a tuple of unique free type parameters of a generic�# type, for example, Dict[T, T].__parameters__ == (T,)

Therefore, for generics like Foo(Generic[T1, *Ts, T2, Ts2]), Ts (a TypeVarTuple) would also need to appear in __parameters__.

The main wrinkle is that callers currently expect Generic[...].__parameters__ to be TypeVars. There are quite a few isinstance(foo, TypeVar) in typing.py and typing_extensions.py (and in other typing-related packages, such as MonkeyType).

We would probably have to make an TypeVarTuple a subclass of TypeVar. That should be fine.

16 of 17

__parameters__

But what about class Foo(Generic[T1, Unpack[Ts], T2, Ts2])?

This is trickier. We'd have to change the invariant for __parameters__ to have a mixture of TypeVars and Unpack. This would be a breaking change for third-party packages that expect __parameters__ to be TypeVars.

Also, Unpack can take a partially or fully concrete parameter, like Tuple[int, Unpack[Ts]].

17 of 17

3. We'd also need to change a substitution function in typing.py that is used for instantiating a generic alias.

4. I'd be curious to find out if there are any other expected behaviors from Generic or TypeVar.