Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-47097: Add documentation for TypeVarTuple #32103

Merged
merged 14 commits into from
Apr 4, 2022
119 changes: 119 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ annotations. These include:
*Introducing* :class:`ParamSpec` and :data:`Concatenate`
* :pep:`613`: Explicit Type Aliases
*Introducing* :data:`TypeAlias`
* :pep:`646`: Variadic Generics
*Introducing* :data:`TypeVarTuple`
* :pep:`647`: User-Defined Type Guards
*Introducing* :data:`TypeGuard`
* :pep:`673`: Self type
Expand Down Expand Up @@ -1230,6 +1232,123 @@ These are not used in annotations. They are building blocks for creating generic
``covariant=True`` or ``contravariant=True``. See :pep:`484` for more
details. By default, type variables are invariant.

.. class:: TypeVarTuple

Type variable tuple. A specialised form of :class:`Type variable <TypeVar>`
mrahtz marked this conversation as resolved.
Show resolved Hide resolved
that enables *variadic* generics.

A normal type variable enables parameterization with a single type. A type
variable tuple, in contrast, allows parameterization with an
*arbitrary* number of types by acting like an *arbitrary* number of type
mrahtz marked this conversation as resolved.
Show resolved Hide resolved
variables wrapped in a tuple. For example::

T = TypeVar('T')
Ts = TypeVarTuple('Ts')

def remove_first_element(tup: tuple[T, *Ts]) -> tuple[*Ts]:
return tup[1:]

# T is bound to int, Ts is bound to ()
# Return value is (), which has type tuple[()]
remove_first_element(tup=(1,))

mrahtz marked this conversation as resolved.
Show resolved Hide resolved
# T is bound to int, Ts is bound to (str,)
# Return value is ('spam',), which has type tuple[str]
remove_first_element(tup=(1, 'spam'))

# T is bound to int, Ts is bound to (str, float)
# Return value is ('spam', 3.0), which has type tuple[str, float]
remove_first_element(tup=(1, 'spam', 3.0))

Note the use of the unpacking operator ``*`` in ``tuple[T, *Ts]``.
Conceptually, you can think of ``Ts`` as a tuple of type variables
``(T1, T2, ...)``. ``tuple[T, *Ts]`` would then become
``tuple[T, *(T1, T2, ...)]``, which is equivalent to
``tuple[T, T1, T2, ...]``. (Note that in older versions of Python, you might
see this written using :data:`Unpack <Unpack>` instead, as
``Unpack[Ts]``.)

Type variable tuples must *always* be unpacked. This helps distinguish type
variable types from normal type variables::

x: Ts # Not valid
x: tuple[Ts] # Not valid
x: tuple[*Ts] # The correct way to to do it

Type variable tuples can be used in the same contexts as normal type
variables. For example, in class definitions, arguments, and return types::

Shape = TypeVarTuple('Shape')
class Array(Generic[*Shape]):
def __getitem__(self, key: tuple[*Shape]) -> float: ...
def __abs__(self) -> Array[*Shape]: ...
def get_shape(self) -> tuple[*Shape]: ...

Type variable tuples can be happily combined with normal type variables::

DType = TypeVar('DType')

class Array(Generic[DType, *Shape]): # This is fine
pass

class Array2(Generic[*Shape, DType]): # This would also be fine
pass

float_array_1d: Array[float, Height] = Array() # Totally fine
int_array_2d: Array[int, Height, Width] = Array() # Yup, fine too

However, note that at most one type variable tuple may appear in a single
list of type arguments or type parameters::

x: tuple[*Ts, *Ts] # Not valid
class Array(Generic[*Shape, *Shape]): # Not valid
pass
mrahtz marked this conversation as resolved.
Show resolved Hide resolved

Finally, an unpacked type variable tuple can be used as the type annotation
of ``*args``::

def call_soon(
callback: Callable[[*Ts], None],
*args: *Ts
) -> None:
...
callback(*args)

In contrast to non-unpacked annotations of ``*args`` - e.g. ``*args: int``,
which would specify that *all* arguments are ``int`` - ``*args: *Ts``
enables reference to the types of the *individual* arguments in ``*args``.
Here, this allows us to ensure the types of the ``*args`` passed
to ``call_soon`` match the types of the (positional) arguments of
``callback``.

For more details on type variable tuples, see :pep:`646`.
mrahtz marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 3.11

.. data:: Unpack

A typing operator that conceptually marks an object as having been
unpacked. For example, using the unpack operator ``*`` on a
:class:`type variable tuple <TypeVarTuple>` internally uses ``Unpack``
mrahtz marked this conversation as resolved.
Show resolved Hide resolved
to mark the type variable tuple as having been unpacked::

Ts = TypeVarTuple('Ts')
tup: tuple[*Ts]
# Effectively does:
tup: tuple[Unpack[Ts]]

In fact, ``Unpack`` can be used interchangeably with ``*`` in the context
of types. You might see ``Unpack`` being used explicitly in older versions
of Python, where ``*`` couldn't be used in certain places::

# In older versions of Python, TypeVarTuple and Unpack
# are located in the `typing_extensions` backports package.
from typing_extensions import TypeVarTuple, Unpack
mrahtz marked this conversation as resolved.
Show resolved Hide resolved

Ts = TypeVarTuple('Ts')
tup: tuple[*Ts] # Syntax error on Python <= 3.10!
tup: tuple[Unpack[Ts]] # Semantically equivalent, and backwards-compatible

.. class:: ParamSpec(name, *, bound=None, covariant=False, contravariant=False)

Parameter specification variable. A specialized version of
Expand Down