Generics, Type[T], and TypeVarTuple

I have a typing and API design question.

With non-variadic generics, I can write something like the following:

from typing import Generic, Type, TypeVar

T = TypeVar("T")

class Example(Generic[T]):
    atype: Type[T]

    def __init__(self, atype: Type[T]):
        self.atype = atype

    def get_value(self) -> T:
        # I can use self.atype in here
        ...

Then later when instantiating Example, a type checker can infer T based on the argument passed to the initializer:

example = Example(int)
n = example.get_value()  # inferred as 'int'

I’ve used this to do things like parsing and validation of text when I want the parsed value to be typed correctly.

Recently, I was experimenting with something similar for multiple values. Homogenous collections are pretty simple since you can parameterize on the element type:

class SeqExample(Generic[T]):
    element_type: Type[T]
    def __init__(self, etype: Type[T]):
        self.element_type = etype
    def get_value(self) -> list[T]:
        ...

list_example = SeqExample(int)
ns = list_example.get_value() # inferred as 'list[int]'

I was experimenting with writing a version for fixed-length tuples, imagining a usage like this:

tup_example = TupExample(int, str, bool)
n, s, b = tup_example.get_value() # inferred as `tuple[int, str, bool]`

This led me to TypeVarTuple, which I had not used before and found very cool, since I could get as far as this:

Ts = TypeVarTuple("Ts")

class TupExample(Generic[*Ts]):
    # ...?
    def get_value(self) -> tuple[*Ts]:
        ...

My questions are:

  • Is even possible to do something like what I do with T/Type[T] with TypeVarTuple? (I am thinking not, but I want to know for sure)
  • Does anyone have ideas or suggestions for implementing the kind of API I’m interested in? I can easily enough implement an un-hinted version by storing tuple[type, ...], but then I lose inference of the method return type based on the arguments to __init__ and have to specify the member types twice, once for the generic parameters and once for the init parameters:
Ts = TypeVarTuple("Ts")

class TupExample(Generic[*Ts]):
    element_types: tuple[type, ...]
    def __init__(self, *etypes: type):
        self.element_types = etypes
    def get_value(self) -> tuple[*Ts]:
        ...

# duplication suboptimal...
tup_example = TupExample[int, str, bool](int, str, bool)
n, s, b = tup_example.get_value() # inferred `tuple[int, str, bool]`, yay!

I’m not exactly sure what you’re trying to do, but the following example code from PEP 646 looks pretty close to your own code:

class Array(Generic[*Shape]):

    def __init__(self, shape: Tuple[*Shape]):
        self._shape: Tuple[*Shape] = shape

    def get_shape(self) -> Tuple[*Shape]:
        return self._shape

shape = (Height(480), Width(640))
x: Array[Height, Width] = Array(shape)

Hi David, thanks for your response!

It is very similar. The crux of the problem is that in the example the same type parameter is used for the initializer parameter, the object attribute, and the method return type (Tuple[*Shape]). In my case, the type of the initializer parameter is different from (but related to) the method return type, because I am initializing with classes but returning values (instances of the classes that were passed to the initializer.)

A non-generic (type parameters replaced with concrete types) version of what I’m trying to do would look something like this:

class TupleOf:
  member_types: tuple[type[int], type[str], type[bool]]

  def __init__(self, types: tuple[type[int], type[str], type[bool]]):
    self.member_types = types

  def get_values(self, tokens: list[str]) -> tuple[int, str, bool]:
    values = []
    for cls in self.member_types:
      raw = tokens.pop(0)
      values.append(cls(raw))
    return tuple(values)

I don’t think there is a way to “map” the Type special form over the contents of a TypeVarTuple, so that one can annotate both the type of a value ad the type itself like with TypeVar and Type[T].