Skip to content

Commit c0e0364

Browse files
committed
gh-148945: add support for bounds on type variable tuples and parameter specifications
Type parameter lists now accept a bound on type variable tuples and parameter specifications, using the same colon syntax already available for TypeVar: def call[*Ts: int, **P: [str]](*args: *Ts, f: Callable[P, int]): ... A TypeVarTuple bound applies to each type the type variable tuple stands for and is a star_expression, like its default. A ParamSpec bound is a parameter list. As with TypeVar bounds, they are lazily evaluated in a separate annotation scope and are exposed through __bound__ and the evaluate_bound evaluate function. The ast.TypeVarTuple and ast.ParamSpec nodes gain a 'bound' field. Also normalizes ParamSpec('P').__bound__ to None; it was previously types.NoneType, unlike TypeVar and TypeVarTuple.
1 parent bc31217 commit c0e0364

25 files changed

Lines changed: 1112 additions & 614 deletions

Doc/library/ast.rst

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,22 +1914,27 @@ aliases.
19141914
.. versionchanged:: 3.13
19151915
Added the *default_value* parameter.
19161916

1917-
.. class:: ParamSpec(name, default_value)
1917+
.. class:: ParamSpec(name, bound, default_value)
19181918

19191919
A :class:`typing.ParamSpec`. ``name`` is the name of the parameter specification.
1920-
``default_value`` is the default value; if the :class:`!ParamSpec` has no default,
1921-
this attribute will be set to ``None``.
1920+
``bound`` is the bound, if any; a parameter specification is bounded by a
1921+
parameter list, so the bound is usually a :class:`List`. ``default_value``
1922+
is the default value; if the :class:`!ParamSpec` has no bound or no default,
1923+
the corresponding attribute will be set to ``None``.
19221924

19231925
.. doctest::
19241926

1925-
>>> print(ast.dump(ast.parse("type Alias[**P = [int, str]] = Callable[P, int]"), indent=4))
1927+
>>> print(ast.dump(ast.parse("type Alias[**P: [int] = [int, str]] = Callable[P, int]"), indent=4))
19261928
Module(
19271929
body=[
19281930
TypeAlias(
19291931
name=Name(id='Alias', ctx=Store()),
19301932
type_params=[
19311933
ParamSpec(
19321934
name='P',
1935+
bound=List(
1936+
elts=[
1937+
Name(id='int')]),
19331938
default_value=List(
19341939
elts=[
19351940
Name(id='int'),
@@ -1946,21 +1951,29 @@ aliases.
19461951
.. versionchanged:: 3.13
19471952
Added the *default_value* parameter.
19481953

1949-
.. class:: TypeVarTuple(name, default_value)
1954+
.. versionchanged:: 3.16
1955+
Added the *bound* parameter.
1956+
1957+
.. class:: TypeVarTuple(name, bound, default_value)
19501958

19511959
A :class:`typing.TypeVarTuple`. ``name`` is the name of the type variable tuple.
1952-
``default_value`` is the default value; if the :class:`!TypeVarTuple` has no
1953-
default, this attribute will be set to ``None``.
1960+
``bound`` is the bound, if any, which applies to each type substituted for the
1961+
type variable tuple. ``default_value`` is the default value; if the
1962+
:class:`!TypeVarTuple` has no bound or no default, the corresponding attribute
1963+
will be set to ``None``.
19541964

19551965
.. doctest::
19561966

1957-
>>> print(ast.dump(ast.parse("type Alias[*Ts = ()] = tuple[*Ts]"), indent=4))
1967+
>>> print(ast.dump(ast.parse("type Alias[*Ts: int = ()] = tuple[*Ts]"), indent=4))
19581968
Module(
19591969
body=[
19601970
TypeAlias(
19611971
name=Name(id='Alias', ctx=Store()),
19621972
type_params=[
1963-
TypeVarTuple(name='Ts', default_value=Tuple())],
1973+
TypeVarTuple(
1974+
name='Ts',
1975+
bound=Name(id='int'),
1976+
default_value=Tuple())],
19641977
value=Subscript(
19651978
value=Name(id='tuple'),
19661979
slice=Tuple(
@@ -1973,6 +1986,9 @@ aliases.
19731986
.. versionchanged:: 3.13
19741987
Added the *default_value* parameter.
19751988

1989+
.. versionchanged:: 3.16
1990+
Added the *bound* parameter.
1991+
19761992
Function and class definitions
19771993
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
19781994

Doc/library/typing.rst

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2109,6 +2109,30 @@ without the dedicated syntax, as documented below.
21092109

21102110
.. versionadded:: 3.15
21112111

2112+
.. attribute:: __bound__
2113+
2114+
The upper bound of each of the types the type variable tuple stands for,
2115+
if any.
2116+
2117+
.. versionchanged:: 3.16
2118+
2119+
For type variable tuples created through
2120+
:ref:`type parameter syntax <type-params>`, the bound is evaluated only
2121+
when the attribute is accessed, not when the type variable tuple is
2122+
created (see :ref:`lazy-evaluation`).
2123+
2124+
.. method:: evaluate_bound
2125+
2126+
An :term:`evaluate function` corresponding to the
2127+
:attr:`~TypeVarTuple.__bound__` attribute.
2128+
When called directly, this method supports only the :attr:`~annotationlib.Format.VALUE`
2129+
format, which is equivalent to accessing the :attr:`~TypeVarTuple.__bound__` attribute
2130+
directly, but the method object can be passed to
2131+
:func:`annotationlib.call_evaluate_function` to evaluate the value in a
2132+
different format.
2133+
2134+
.. versionadded:: 3.16
2135+
21122136
.. attribute:: __default__
21132137

21142138
The default value of the type variable tuple, or :data:`typing.NoDefault` if it
@@ -2135,11 +2159,6 @@ without the dedicated syntax, as documented below.
21352159

21362160
.. versionadded:: 3.13
21372161

2138-
Type variable tuples created with ``covariant=True`` or
2139-
``contravariant=True`` can be used to declare covariant or contravariant
2140-
generic types. The ``bound`` argument is also accepted, similar to
2141-
:class:`TypeVar`, but its actual semantics are yet to be decided.
2142-
21432162
.. versionadded:: 3.11
21442163

21452164
.. versionchanged:: 3.12
@@ -2156,6 +2175,12 @@ without the dedicated syntax, as documented below.
21562175
Added support for the ``bound``, ``covariant``, ``contravariant``, and
21572176
``infer_variance`` parameters.
21582177

2178+
.. versionchanged:: 3.16
2179+
2180+
Type variable tuple bounds can now be declared using the
2181+
:ref:`type parameter <type-params>` syntax, and are
2182+
:ref:`lazily evaluated <lazy-evaluation>`.
2183+
21592184
.. class:: ParamSpec(name, *, bound=None, covariant=False, contravariant=False, infer_variance=False, default=typing.NoDefault)
21602185

21612186
Parameter specification variable. A specialized version of
@@ -2239,6 +2264,34 @@ without the dedicated syntax, as documented below.
22392264

22402265
.. versionadded:: 3.12
22412266

2267+
.. attribute:: __bound__
2268+
2269+
The upper bound of the parameter specification, if any. Because a
2270+
parameter specification stands for the parameters of a callable, its
2271+
bound is a parameter list, such as ``[int, str]``.
2272+
2273+
.. versionchanged:: 3.16
2274+
2275+
For parameter specifications created through
2276+
:ref:`type parameter syntax <type-params>`, the bound is evaluated only
2277+
when the attribute is accessed, not when the parameter specification is
2278+
created (see :ref:`lazy-evaluation`).
2279+
2280+
Previously, :attr:`!__bound__` was :class:`types.NoneType` rather than
2281+
``None`` when no bound was given.
2282+
2283+
.. method:: evaluate_bound
2284+
2285+
An :term:`evaluate function` corresponding to the
2286+
:attr:`~ParamSpec.__bound__` attribute.
2287+
When called directly, this method supports only the :attr:`~annotationlib.Format.VALUE`
2288+
format, which is equivalent to accessing the :attr:`~ParamSpec.__bound__` attribute
2289+
directly, but the method object can be passed to
2290+
:func:`annotationlib.call_evaluate_function` to evaluate the value in a
2291+
different format.
2292+
2293+
.. versionadded:: 3.16
2294+
22422295
.. attribute:: __default__
22432296

22442297
The default value of the parameter specification, or :data:`typing.NoDefault` if it
@@ -2267,8 +2320,7 @@ without the dedicated syntax, as documented below.
22672320

22682321
Parameter specification variables created with ``covariant=True`` or
22692322
``contravariant=True`` can be used to declare covariant or contravariant
2270-
generic types. The ``bound`` argument is also accepted, similar to
2271-
:class:`TypeVar`. However the actual semantics of these keywords are yet to
2323+
generic types. However the actual semantics of these keywords are yet to
22722324
be decided.
22732325

22742326
.. versionadded:: 3.10
@@ -2282,6 +2334,12 @@ without the dedicated syntax, as documented below.
22822334

22832335
Support for default values was added.
22842336

2337+
.. versionchanged:: 3.16
2338+
2339+
Parameter specification bounds can now be declared using the
2340+
:ref:`type parameter <type-params>` syntax, and are
2341+
:ref:`lazily evaluated <lazy-evaluation>`.
2342+
22852343
.. note::
22862344
Only parameter specification variables defined in global scope can
22872345
be pickled.

Doc/reference/compound_stmts.rst

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,8 +1768,8 @@ Type parameter lists
17681768
type_params: "[" `type_param` ("," `type_param`)* "]"
17691769
type_param: `typevar` | `typevartuple` | `paramspec`
17701770
typevar: `identifier` (":" `expression`)? ("=" `expression`)?
1771-
typevartuple: "*" `identifier` ("=" `expression`)?
1772-
paramspec: "**" `identifier` ("=" `expression`)?
1771+
typevartuple: "*" `identifier` (":" `starred_expression`)? ("=" `starred_expression`)?
1772+
paramspec: "**" `identifier` (":" `expression`)? ("=" `expression`)?
17731773

17741774
:ref:`Functions <def>` (including :ref:`coroutines <async def>`),
17751775
:ref:`classes <class>` and :ref:`type aliases <type>` may
@@ -1832,8 +1832,19 @@ but only when the value is explicitly accessed through the attributes ``__bound_
18321832
and ``__constraints__``. To accomplish this, the bounds or constraints are
18331833
evaluated in a separate :ref:`annotation scope <annotation-scopes>`.
18341834

1835-
:data:`typing.TypeVarTuple`\ s and :data:`typing.ParamSpec`\ s cannot have bounds
1836-
or constraints.
1835+
:data:`typing.TypeVarTuple`\ s and :data:`typing.ParamSpec`\ s can also declare a
1836+
bound with a colon (``:``) followed by an expression, but they cannot declare
1837+
constraints. For a :data:`!typing.TypeVarTuple`, the bound applies to each of the
1838+
types it stands for (e.g. in ``*Ts: int``, every type substituted for ``Ts`` must
1839+
be a subtype of :class:`int`). For a :data:`!typing.ParamSpec`, the bound is a
1840+
parameter list that the substituted parameters must be compatible with (e.g.
1841+
``**P: [int]``). As with :data:`!typing.TypeVar`, these bounds are lazily
1842+
evaluated in a separate :ref:`annotation scope <annotation-scopes>` and are not
1843+
enforced at runtime.
1844+
1845+
.. versionchanged:: 3.16
1846+
Added support for bounds on :data:`!typing.TypeVarTuple`\ s and
1847+
:data:`!typing.ParamSpec`\ s.
18371848

18381849
All three flavors of type parameters can also have a *default value*, which is used
18391850
when the type parameter is not explicitly provided. This is added by appending
@@ -1853,7 +1864,9 @@ The following example indicates the full set of allowed type parameter declarati
18531864
TypeVarWithBound: int,
18541865
TypeVarWithConstraints: (str, bytes),
18551866
*SimpleTypeVarTuple = (int, float),
1867+
*TypeVarTupleWithBound: int,
18561868
**SimpleParamSpec = (str, bytearray),
1869+
**ParamSpecWithBound: [int],
18571870
](
18581871
a: SimpleTypeVar,
18591872
b: TypeVarWithDefault,

Doc/whatsnew/3.16.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ New features
7575
Other language changes
7676
======================
7777

78+
* :ref:`Type parameter lists <type-params>` now accept bounds on type variable
79+
tuples and parameter specifications, using the same syntax already available
80+
for :class:`~typing.TypeVar`::
81+
82+
def call[*Ts: int, **P: [str]](*args: *Ts, f: Callable[P, int]) -> None: ...
83+
84+
A :class:`~typing.TypeVarTuple` bound applies to each type the type variable
85+
tuple stands for, while a :class:`~typing.ParamSpec` bound is a parameter
86+
list. Like other type parameter bounds, they are
87+
:ref:`lazily evaluated <lazy-evaluation>` and are available through the
88+
``__bound__`` attribute and the ``evaluate_bound``
89+
:term:`evaluate function`.
90+
(Contributed by KotlinIsland in :gh:`148945`.)
91+
7892
* :meth:`memoryview.cast` now allows casting a multidimensional
7993
F-contiguous view to a one-dimensional view.
8094
(Contributed by Jaemin Park in :gh:`91484`.)

Grammar/python.gram

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -694,11 +694,14 @@ type_param_seq[asdl_type_param_seq*]: a[asdl_type_param_seq*]=','.type_param+ ['
694694

695695
type_param[type_param_ty] (memo):
696696
| a=NAME b=[type_param_bound] c=[type_param_default] { _PyAST_TypeVar(a->v.Name.id, b, c, EXTRA) }
697-
| invalid_type_param
698-
| '*' a=NAME b=[type_param_starred_default] { _PyAST_TypeVarTuple(a->v.Name.id, b, EXTRA) }
699-
| '**' a=NAME b=[type_param_default] { _PyAST_ParamSpec(a->v.Name.id, b, EXTRA) }
697+
| '*' a=NAME b=[type_param_starred_bound] c=[type_param_starred_default] { _PyAST_TypeVarTuple(a->v.Name.id, b, c, EXTRA) }
698+
| '**' a=NAME b=[type_param_paramspec_bound] c=[type_param_default] { _PyAST_ParamSpec(a->v.Name.id, b, c, EXTRA) }
700699

701700
type_param_bound[expr_ty]: ':' e=expression { e }
701+
type_param_starred_bound[expr_ty]: ':' e=star_expression {
702+
CHECK_VERSION(expr_ty, 16, "Type variable tuple bounds are", e) }
703+
type_param_paramspec_bound[expr_ty]: ':' e=expression {
704+
CHECK_VERSION(expr_ty, 16, "Parameter specification bounds are", e) }
702705
type_param_default[expr_ty]: '=' e=expression {
703706
CHECK_VERSION(expr_ty, 13, "Type parameter defaults are", e) }
704707
type_param_starred_default[expr_ty]: '=' e=star_expression {
@@ -1249,18 +1252,6 @@ invalid_legacy_expression:
12491252
_PyPegen_check_legacy_stmt(p, a) ? RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b,
12501253
"Missing parentheses in call to '%U'. Did you mean %U(...)?", a->v.Name.id, a->v.Name.id) : NULL}
12511254

1252-
invalid_type_param:
1253-
| '*' a=NAME colon=':' e=expression {
1254-
RAISE_SYNTAX_ERROR_STARTING_FROM(colon, e->kind == Tuple_kind
1255-
? "cannot use constraints with TypeVarTuple"
1256-
: "cannot use bound with TypeVarTuple")
1257-
}
1258-
| '**' a=NAME colon=':' e=expression {
1259-
RAISE_SYNTAX_ERROR_STARTING_FROM(colon, e->kind == Tuple_kind
1260-
? "cannot use constraints with ParamSpec"
1261-
: "cannot use bound with ParamSpec")
1262-
}
1263-
12641255
invalid_expression:
12651256
| STRING a=(!STRING expression_without_invalid)+ STRING {
12661257
RAISE_SYNTAX_ERROR_KNOWN_RANGE( PyPegen_first_item(a, expr_ty), PyPegen_last_item(a, expr_ty),

Include/internal/pycore_ast.h

Lines changed: 9 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_intrinsics.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@
3131
#define INTRINSIC_SET_FUNCTION_TYPE_PARAMS 4
3232
#define INTRINSIC_SET_TYPEPARAM_DEFAULT 5
3333
#define INTRINSIC_ADD_CONDITIONAL_ANNOTATION 6
34+
#define INTRINSIC_TYPEVARTUPLE_WITH_BOUND 7
35+
#define INTRINSIC_PARAMSPEC_WITH_BOUND 8
3436

35-
#define MAX_INTRINSIC_2 6
37+
#define MAX_INTRINSIC_2 8
3638

3739
typedef PyObject *(*intrinsic_func1)(PyThreadState* tstate, PyObject *value);
3840
typedef PyObject *(*intrinsic_func2)(PyThreadState* tstate, PyObject *value1, PyObject *value2);

Include/internal/pycore_magic_number.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ Known values:
303303
Python 3.16a1 3703 (Replace DELETE_GLOBAL with PUSH_NULL; STORE_GLOBAL)
304304
Python 3.16a1 3704 (Replace DELETE_ATTR with PUSH_NULL; STORE_ATTR)
305305
Python 3.16a1 3705 (Add INTRINSIC_ADD_CONDITIONAL_ANNOTATION)
306+
Python 3.16a1 3706 (Add INTRINSIC_TYPEVARTUPLE_WITH_BOUND and INTRINSIC_PARAMSPEC_WITH_BOUND)
306307
307308
Python 3.17 will start with 3750
308309
@@ -312,7 +313,7 @@ Known values:
312313
313314
*/
314315

315-
#define PYC_MAGIC_NUMBER 3705
316+
#define PYC_MAGIC_NUMBER 3706
316317
/* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
317318
(little-endian) and then appending b'\r\n'. */
318319
#define PYC_MAGIC_NUMBER_TOKEN \

Include/internal/pycore_typevarobject.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ extern "C" {
1010

1111
extern PyObject *_Py_make_typevar(PyObject *, PyObject *, PyObject *);
1212
extern PyObject *_Py_make_paramspec(PyThreadState *, PyObject *);
13+
extern PyObject *_Py_make_paramspec_with_bound(PyObject *, PyObject *);
1314
extern PyObject *_Py_make_typevartuple(PyThreadState *, PyObject *);
15+
extern PyObject *_Py_make_typevartuple_with_bound(PyObject *, PyObject *);
1416
extern PyObject *_Py_make_typealias(PyThreadState *, PyObject *);
1517
extern PyObject *_Py_subscript_generic(PyThreadState *, PyObject *);
1618
extern PyObject *_Py_set_typeparam_default(PyThreadState *, PyObject *, PyObject *);

Lib/_ast_unparse.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,12 +453,18 @@ def visit_TypeVar(self, node):
453453

454454
def visit_TypeVarTuple(self, node):
455455
self.write("*" + node.name)
456+
if node.bound:
457+
self.write(": ")
458+
self.traverse(node.bound)
456459
if node.default_value:
457460
self.write(" = ")
458461
self.traverse(node.default_value)
459462

460463
def visit_ParamSpec(self, node):
461464
self.write("**" + node.name)
465+
if node.bound:
466+
self.write(": ")
467+
self.traverse(node.bound)
462468
if node.default_value:
463469
self.write(" = ")
464470
self.traverse(node.default_value)

0 commit comments

Comments
 (0)