-
Notifications
You must be signed in to change notification settings - Fork 243
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added draft chapter to typing spec for tuples. This consolidates and …
…augments existing information about tuples within the type system.
- Loading branch information
Showing
4 changed files
with
145 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
Tuples | ||
====== | ||
|
||
The ``tuple`` class has some special behaviors and properties that make it | ||
different from other classes from a typing perspective. The most obvious | ||
difference is that ``tuple`` is variadic -- it supports an arbitrary number | ||
of type arguments. Each type argument represents a single element, and those | ||
elements are immutable at runtime. These properties affect subtyping rules and | ||
other behaviors as described below. | ||
|
||
|
||
Tuple Type Form | ||
--------------- | ||
|
||
The type of a tuple can be expressed by listing the element types. For | ||
example, ``tuple[int, int, str]`` is a tuple containing an ``int``, another | ||
``int``, and a ``str``. | ||
|
||
The empty tuple can be annotated as ``tuple[()]``. | ||
|
||
Arbitrary-length homogeneous tuples can be expressed using one type and | ||
ellipsis, for example ``tuple[int, ...]``. This type is equivalent to a union | ||
of tuples containing zero or more ``int`` elements (``tuple[()] | | ||
tuple[int] | tuple[int, int] | tuple[int, int, int] | ...``). | ||
Arbitrary-length homogeneous tuples are sometimes referred to as "unbounded | ||
tuples". Both of these terms appear within the typing spec, and they refer to | ||
the same concept. | ||
|
||
The type ``tuple[Any, ...]`` is special in that it is bidirectionally | ||
compatible with any tuple of any length. This is useful for gradual typing. | ||
The type ``tuple`` (with no type arguments provided) is equivalent to | ||
``tuple[Any, ...]``. | ||
|
||
|
||
Unpacked Tuple Form | ||
------------------- | ||
|
||
An unpacked form of ``tuple`` (using an unpack operator ``*``) can be used | ||
within a tuple type argument list. For example, ``tuple[int, *tuple[str]]`` | ||
is equivalent to ``tuple[int, str]``. Unpacking an unbounded tuple preserves | ||
the unbounded tuple as it is. That is, ``*tuple[int, ...]`` remains | ||
``*tuple[int, ...]``; there's no simpler form. This enables us to specify | ||
types such as ``tuple[int, *tuple[str, ...], str]`` -- a tuple type where the | ||
first element is guaranteed to be of type ``int``, the last element is | ||
guaranteed to be of type ``str``, and the elements in the middle are zero or | ||
more elements of type ``str``. The type ``tuple[*tuple[int, ...]]`` is | ||
equivalent to ``tuple[int, ...]``. | ||
|
||
If an unpacked ``*tuple[Any, ...]`` is embedded within another tuple, that | ||
portion of the tuple is bidirectionally type compatible with any tuple of | ||
any length. | ||
|
||
Only one unbounded tuple can be used with another tuple:: | ||
|
||
t1: tuple[*tuple[str], *tuple[str]] # OK | ||
t2: tuple[*tuple[str, *tuple[str, ...]]] # OK | ||
t3: tuple[*tuple[str, ...], *tuple[int, ...]] # Type error | ||
t4: tuple[*tuple[str, *tuple[str, ...]], *tuple[int, ...]] # Type error | ||
|
||
An unpacked TypeVarTuple counts as an unbounded tuple in the context of this rule:: | ||
|
||
def func[*Ts](t: tuple[*Ts]): | ||
t5: tuple[*tuple[str], *Ts] # OK | ||
t6: tuple[*tuple[str, ...], *Ts] # Type error | ||
|
||
The ``*`` syntax requires Python 3.11 or newer. For older versions of Python, | ||
the ``typing.Unpack`` special form can be used: | ||
``tuple[int, Unpack[tuple[str, ...]], int]``. | ||
|
||
Unpacked tuples can also be used for ``*args`` parameters in a function | ||
signature. For example, ``def f(*args: *tuple[int, str]): ...``. Unpacked tuples | ||
can also be used for specializing generic classes or type variables that are | ||
parameterized using a ``TypeVarTuple``. For more details, see "``*args`` as a | ||
Type Variable Tuple" in the Generics section. [TODO: not sure how to create a | ||
link to a heading in another .rst file] | ||
|
||
|
||
Type Compatibility Rules | ||
------------------------ | ||
|
||
Because tuple contents are immutable, the element types of a tuple are covariant. | ||
For example, ``tuple[int, int]`` is a subtype of ``tuple[float, complex]``. | ||
|
||
As discussed above, a homogeneous tuple of arbitrary length is equivalent | ||
to a union of tuples of different lengths. That means ``tuple[()]``, | ||
``tuple[int]`` and ``tuple[int, *tuple[int, ...]]`` are all subtypes of | ||
``tuple[int, ...]``. | ||
|
||
The type ``tuple[Any, ...]`` is bidirectionally compatible with any tuple:: | ||
|
||
def func(t1: tuple[int], t2: tuple[int, ...], t3: tuple[Any, ...]): | ||
v1: tuple[int, ...] = t1 # OK | ||
v2: tuple[Any, ...] = t1 # OK | ||
|
||
v3: tuple[int] = t2 # Type error | ||
v4: tuple[Any, ...] = t2 # OK | ||
|
||
v5: tuple[float, float] = t3 # OK | ||
v6: tuple[int, *tuple[str, ...]] = t3 # OK | ||
|
||
|
||
The length of a tuple at runtime is immutable, so it is safe for type checkers | ||
to use length checks to narrow the type of a tuple:: | ||
|
||
def func(val: tuple[int] | tuple[str, str] | tuple[int, *tuple[str], int]): | ||
if len(val) == 1: | ||
# Type can be narrowed to tuple[int]. | ||
reveal_type(val) # tuple[int] | ||
if len(val) == 2: | ||
# Type can be narrowed to tuple[str, str] | tuple[int, int]. | ||
reveal_type(val) # tuple[str, str] | tuple[int, int] | ||
|
||
if len(val) == 3: | ||
# Type can be narrowed to tuple[int, str, int]. | ||
reveal_type(val) # tuple[int, str, int] | ||
|
||
This property may also be used to safely narrow tuple types within a ``match`` | ||
statement that uses sequence patterns. | ||
|
||
If a tuple element is a union type, the tuple can be safely expanded into a | ||
union of tuples. For example, ``tuple[int | str]`` is equivalent to | ||
``tuple[int] | tuple[str]``. If multiple elements are union types, the expansion | ||
must consider all combinations. For example, ``tuple[int | str, int | str]`` is | ||
equivalent to ``tuple[int, int] | tuple[int, str] | tuple[str, int] | tuple[str, str]``. | ||
Unbounded tuples cannot be expanded in this manner. | ||
|
||
Type checkers may safely use this equivalency rule when narrowing tuple types:: | ||
|
||
def func(subj: tuple[int | str, int | str]): | ||
match subj: | ||
case x, str(): | ||
reveal_type(subj) # tuple[int | str, str] | ||
case y: | ||
reveal_type(subj) # tuple[int | str, int] | ||
|
||
The ``tuple`` class derives from ``Sequence[T_co]`` where ``T_co`` is a covariant | ||
(non-variadic) type variable. The specialized type of ``T_co`` should be computed | ||
by a type checker based on the union of element types within the tuple. | ||
For example, ``tuple[int, *tuple[str, ...]]`` is a subtype of ``Sequence[int | str]``. | ||
A zero-length tuple (``tuple[()]``) is a subtype of ``Sequence[Never]``. | ||
|
||
|