Search code examples
pythonmypypython-typingpylance

Can the * (unpacking) operator be typed in Python? Or any other variadic args function such that all variadic types are in the result type?


Working with type stubs, I'm wondering if it's possible to express a type in Python that allows you to type this correctly for any number of arguments:

def test(*args):
  return args

At first glance, I came with:

T = TypeVar('T')
def test(*args: T) -> Tuple[T, ...]:
  return args

But this of course will only type correctly the first T.

Is the only possible way to write the overrides for all arities?

T1 = TypeVar('T1')
T2 = TypeVar('T2')
T3 = TypeVar('T3')
T4 = TypeVar('T4')

@overload
def test(arg1: T1) -> Tuple[T1]: ...
@overload
def test(arg1: T1, arg2: T2) -> Tuple[T1, T2]: ...
@overload
def test(arg1: T1, arg2: T2, arg3: T3) -> Tuple[T1, T2, T3]: ...
@overload
def test(arg1: T1, arg2: T2, arg3: T3, arg4: T4) -> Tuple[T1, T2, T3, T4]: ...
# etc
def test(*args: Any) -> Tuple[Any, ...]:
  return args

This is not complete either, since it does not carry enough type information to type something like:

x: Tuple[int, int, str] = test(*[1, 2, "4"])

Solution

  • This can solved through TypeVarTuple from PEP646, implemented in Python 3.11 or forward compat module typing-extensions.

    See this analog example:

    from __future__ import annotations
    
    from typing import Any, Callable, Generic
    from typing_extensions import TypeVarTuple, Unpack  # typing_extensions only needed for Python < 3.11
    
    Ts = TypeVarTuple("Ts")
    
    class Signal(Generic[Unpack[Ts]]):
        def add_callback(self, func: Callable[[Unpack[Ts]], Any]) -> None:
            ...
    
        def emit(self, *args: Unpack[Ts]) -> None:
            ...
    
    def callback(a: int, b: str) -> None:
        ...
    
    def callback_bad(a: str) -> None:
        ...
    
    sig: Signal[int, str] = Signal()
    sig.add_callback(callback)  # Good lint
    sig.add_callback(callback_bad)  # Bad lint
    
    sig.emit(1223, "foo")  # Good lint
    sig.emit("bar")  # Bad lint
    

    Also see Dynamic TypeVar for a sequence of types for a solution.