Skip to content

Commit 02fc524

Browse files
authored
Added draft chapter to typing spec for tuples. (#1599)
* Added draft chapter to typing spec for tuples. This consolidates and augments existing information about tuples within the type system. * Added suggested text from @srittau that clarifies illegal forms of unbounded tuples. * Incorporated PR feedback from Jelle. * Incorporated feedback on `Sequence` type compatibility section. * Incorporated PR feedback from @rchen152. * Fixed minor grammatical error. * Incorporated PR feedback from @hauntsaninja.
1 parent 7588639 commit 02fc524

File tree

4 files changed

+158
-38
lines changed

4 files changed

+158
-38
lines changed

docs/spec/generics.rst

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,26 +1228,10 @@ allow unpacking a tuple type. As we shall see, this also enables a
12281228
number of interesting features.
12291229

12301230

1231-
Unpacking Concrete Tuple Types
1232-
""""""""""""""""""""""""""""""
1233-
1234-
Unpacking a concrete tuple type is analogous to unpacking a tuple of
1235-
values at runtime. ``tuple[int, *tuple[bool, bool], str]`` is
1236-
equivalent to ``tuple[int, bool, bool, str]``.
1237-
12381231
Unpacking Unbounded Tuple Types
12391232
"""""""""""""""""""""""""""""""
12401233

1241-
Unpacking an unbounded tuple preserves the unbounded tuple as it is.
1242-
That is, ``*tuple[int, ...]`` remains ``*tuple[int, ...]``; there's no
1243-
simpler form. This enables us to specify types such as ``tuple[int,
1244-
*tuple[str, ...], str]`` - a tuple type where the first element is
1245-
guaranteed to be of type ``int``, the last element is guaranteed to be
1246-
of type ``str``, and the elements in the middle are zero or more
1247-
elements of type ``str``. Note that ``tuple[*tuple[int, ...]]`` is
1248-
equivalent to ``tuple[int, ...]``.
1249-
1250-
Unpacking unbounded tuples is also useful in function signatures where
1234+
Unpacking unbounded tuples is useful in function signatures where
12511235
we don't care about the exact elements and don't want to define an
12521236
unnecessary ``TypeVarTuple``:
12531237

@@ -1302,18 +1286,7 @@ explicitly marking the code as unsafe (by using ``y: Array[*tuple[Any,
13021286
checker every time they tried to use the variable ``y``, which would
13031287
hinder them when migrating a legacy code base to use ``TypeVarTuple``.
13041288

1305-
Multiple Unpackings in a Tuple: Not Allowed
1306-
"""""""""""""""""""""""""""""""""""""""""""
1307-
1308-
As with ``TypeVarTuples``, `only one <Multiple Type Variable Tuples:
1309-
Not Allowed_>`_ unpacking may appear in a tuple:
1310-
1311-
1312-
::
1313-
1314-
x: tuple[int, *Ts, str, *Ts2] # Error
1315-
y: tuple[int, *tuple[int, ...], str, *tuple[str, ...]] # Error
1316-
1289+
.. _args_as_typevartuple:
13171290

13181291
``*args`` as a Type Variable Tuple
13191292
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

docs/spec/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Specification for the Python type system
2020
overload
2121
dataclasses
2222
typeddict
23+
tuples
2324
narrowing
2425
directives
2526
distributing

docs/spec/special-types.rst

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,6 @@ The ``NoReturn`` type is conventionally used in return annotations of
9696
functions, and ``Never`` is typically used in other locations, but the two
9797
types are completely interchangeable.
9898

99-
Tuples
100-
------
101-
102-
The type of a tuple can be expressed by listing the element
103-
types: ``tuple[int, int, str]`` is a tuple containing an int,
104-
another int, and a str. The empty tuple can be typed as
105-
``tuple[()]``. Arbitrary-length homogeneous tuples can be
106-
expressed using one type and ellipsis, for example ``tuple[int, ...]``.
107-
10899
Special cases for ``float`` and ``complex``
109100
-------------------------------------------
110101

docs/spec/tuples.rst

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
Tuples
2+
======
3+
4+
The ``tuple`` class has some special behaviors and properties that make it
5+
different from other classes from a typing perspective. The most obvious
6+
difference is that ``tuple`` is variadic -- it supports an arbitrary number
7+
of type arguments. At runtime, the sequence of objects contained within the
8+
tuple is fixed at the time of construction. Elements cannot be added, removed,
9+
reordered, or replaced after construction. These properties affect subtyping
10+
rules and other behaviors as described below.
11+
12+
13+
Tuple Type Form
14+
---------------
15+
16+
The type of a tuple can be expressed by listing the element types. For
17+
example, ``tuple[int, int, str]`` is a tuple containing an ``int``, another
18+
``int``, and a ``str``.
19+
20+
The empty tuple can be annotated as ``tuple[()]``.
21+
22+
Arbitrary-length homogeneous tuples can be expressed using one type and an
23+
ellipsis, for example ``tuple[int, ...]``. This type is equivalent to a union
24+
of tuples containing zero or more ``int`` elements (``tuple[()] |
25+
tuple[int] | tuple[int, int] | tuple[int, int, int] | ...``).
26+
Arbitrary-length homogeneous tuples are sometimes referred to as "unbounded
27+
tuples". Both of these terms appear within the typing spec, and they refer to
28+
the same concept.
29+
30+
The type ``tuple[Any, ...]`` is special in that it is bidirectionally
31+
compatible with any tuple of any length. This is useful for gradual typing.
32+
The type ``tuple`` (with no type arguments provided) is equivalent to
33+
``tuple[Any, ...]``.
34+
35+
Arbitrary-length tuples have exactly two type arguments -- the type and
36+
an ellipsis. Any other tuple form that uses an ellipsis is invalid::
37+
38+
t1: tuple[int, ...] # OK
39+
t2: tuple[int, int, ...] # Invalid
40+
t3: tuple[...] # Invalid
41+
t4: tuple[..., int] # Invalid
42+
t5: tuple[int, ..., int] # Invalid
43+
t6: tuple[*tuple[str], ...] # Invalid
44+
t7: tuple[*tuple[str, ...], ...] # Invalid
45+
46+
47+
Unpacked Tuple Form
48+
-------------------
49+
50+
An unpacked form of ``tuple`` (using an unpack operator ``*``) can be used
51+
within a tuple type argument list. For example, ``tuple[int, *tuple[str]]``
52+
is equivalent to ``tuple[int, str]``. Unpacking an unbounded tuple preserves
53+
the unbounded tuple as it is. That is, ``*tuple[int, ...]`` remains
54+
``*tuple[int, ...]``; there's no simpler form. This enables us to specify
55+
types such as ``tuple[int, *tuple[str, ...], str]`` -- a tuple type where the
56+
first element is guaranteed to be of type ``int``, the last element is
57+
guaranteed to be of type ``str``, and the elements in the middle are zero or
58+
more elements of type ``str``. The type ``tuple[*tuple[int, ...]]`` is
59+
equivalent to ``tuple[int, ...]``.
60+
61+
If an unpacked ``*tuple[Any, ...]`` is embedded within another tuple, that
62+
portion of the tuple is bidirectionally type compatible with any tuple of
63+
any length.
64+
65+
Only one unbounded tuple can be used within another tuple::
66+
67+
t1: tuple[*tuple[str], *tuple[str]] # OK
68+
t2: tuple[*tuple[str, *tuple[str, ...]]] # OK
69+
t3: tuple[*tuple[str, ...], *tuple[int, ...]] # Type error
70+
t4: tuple[*tuple[str, *tuple[str, ...]], *tuple[int, ...]] # Type error
71+
72+
An unpacked TypeVarTuple counts as an unbounded tuple in the context of this rule::
73+
74+
def func[*Ts](t: tuple[*Ts]):
75+
t5: tuple[*tuple[str], *Ts] # OK
76+
t6: tuple[*tuple[str, ...], *Ts] # Type error
77+
78+
The ``*`` syntax requires Python 3.11 or newer. For older versions of Python,
79+
the ``typing.Unpack`` special form can be used:
80+
``tuple[int, Unpack[tuple[str, ...]], int]``.
81+
82+
Unpacked tuples can also be used for ``*args`` parameters in a function
83+
signature: ``def f(*args: *tuple[int, str]): ...``. Unpacked tuples
84+
can also be used for specializing generic classes or type variables that are
85+
parameterized using a ``TypeVarTuple``. For more details, see
86+
:ref:`args_as_typevartuple`.
87+
88+
89+
Type Compatibility Rules
90+
------------------------
91+
92+
Because tuple contents are immutable, the element types of a tuple are covariant.
93+
For example, ``tuple[int, int]`` is a subtype of ``tuple[float, complex]``.
94+
95+
As discussed above, a homogeneous tuple of arbitrary length is equivalent
96+
to a union of tuples of different lengths. That means ``tuple[()]``,
97+
``tuple[int]`` and ``tuple[int, *tuple[int, ...]]`` are all subtypes of
98+
``tuple[int, ...]``. The converse is not true; ``tuple[int, ...]``` is not a
99+
subtype of ``tuple[int]``.
100+
101+
The type ``tuple[Any, ...]`` is bidirectionally compatible with any tuple::
102+
103+
def func(t1: tuple[int], t2: tuple[int, ...], t3: tuple[Any, ...]):
104+
v1: tuple[int, ...] = t1 # OK
105+
v2: tuple[Any, ...] = t1 # OK
106+
107+
v3: tuple[int] = t2 # Type error
108+
v4: tuple[Any, ...] = t2 # OK
109+
110+
v5: tuple[float, float] = t3 # OK
111+
v6: tuple[int, *tuple[str, ...]] = t3 # OK
112+
113+
114+
The length of a tuple at runtime is immutable, so it is safe for type checkers
115+
to use length checks to narrow the type of a tuple::
116+
117+
def func(val: tuple[int] | tuple[str, str] | tuple[int, *tuple[str, ...], int]):
118+
if len(val) == 1:
119+
# Type can be narrowed to tuple[int].
120+
reveal_type(val) # tuple[int]
121+
122+
if len(val) == 2:
123+
# Type can be narrowed to tuple[str, str] | tuple[int, int].
124+
reveal_type(val) # tuple[str, str] | tuple[int, int]
125+
126+
if len(val) == 3:
127+
# Type can be narrowed to tuple[int, str, int].
128+
reveal_type(val) # tuple[int, str, int]
129+
130+
This property may also be used to safely narrow tuple types within a ``match``
131+
statement that uses sequence patterns.
132+
133+
If a tuple element is a union type, the tuple can be safely expanded into a
134+
union of tuples. For example, ``tuple[int | str]`` is equivalent to
135+
``tuple[int] | tuple[str]``. If multiple elements are union types, full expansion
136+
must consider all combinations. For example, ``tuple[int | str, int | str]`` is
137+
equivalent to ``tuple[int, int] | tuple[int, str] | tuple[str, int] | tuple[str, str]``.
138+
Unbounded tuples cannot be expanded in this manner.
139+
140+
Type checkers may safely use this equivalency rule when narrowing tuple types::
141+
142+
def func(subj: tuple[int | str, int | str]):
143+
match subj:
144+
case x, str():
145+
reveal_type(subj) # tuple[int | str, str]
146+
case y:
147+
reveal_type(subj) # tuple[int | str, int]
148+
149+
The ``tuple`` class derives from ``Sequence[T_co]`` where ``T_co`` is a covariant
150+
(non-variadic) type variable. The specialized type of ``T_co`` should be computed
151+
by a type checker as a a supertype of all element types.
152+
For example, ``tuple[int, *tuple[str, ...]]`` is a subtype of
153+
``Sequence[int | str]`` or ``Sequence[object]``.
154+
155+
A zero-length tuple (``tuple[()]``) is a subtype of ``Sequence[Never]``.

0 commit comments

Comments
 (0)