From be6a3c3a303b44a9e20a939597f243f7080d656b Mon Sep 17 00:00:00 2001 From: KotlinIsland <65446343+kotlinisland@users.noreply.github.com> Date: Thu, 13 Aug 2026 09:26:12 +1000 Subject: [PATCH] gh-140596: Substitute earlier type parameters into PEP 696 defaults The default of a type parameter may refer to type parameters that appear earlier in the same type parameter list. Those references are now replaced by the values supplied for the earlier type parameters when the default is filled in, so ``class Bar[T, S = T]`` gives ``Bar[int, int]`` for ``Bar[int]`` instead of leaving ``S`` bound to the unsubstituted ``T``. Leaving it unsubstituted leaked ``T`` into the specialization, which made ``class Baz[U](Bar[U])`` fail with "Some type variables (T) are not listed in Generic[U]". The substitution happens in ``__typing_prepare_subst__``, where the values of all earlier type parameters are already known, so it applies to ``TypeVar``, ``ParamSpec`` and ``TypeVarTuple`` defaults and to both generic classes and generic aliases. A default that refers to a type parameter which is not declared before it, whether a self-reference (``class A[T = T]``) or a cycle (``class A[T = S, S = T]``), now raises TypeError when the default is used. --- Doc/reference/compound_stmts.rst | 12 ++ Lib/test/test_type_params.py | 108 +++++++++++++++++- Lib/test/test_typing.py | 30 +++++ Lib/typing.py | 64 ++++++++++- ...-08-13-11-42-08.gh-issue-140596.Kq3vLt.rst | 6 + Objects/typevarobject.c | 13 ++- 6 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-13-11-42-08.gh-issue-140596.Kq3vLt.rst diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index e74f3262ed540cd..ce516a117e4bda8 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -1845,6 +1845,18 @@ is accessed. To this end, the default value is evaluated in a separate for a type parameter, the ``__default__`` attribute is set to the special sentinel object :data:`typing.NoDefault`. +A default value may refer to type parameters that appear earlier in the same +type parameter list. Such a reference is replaced by the value that was +supplied for that type parameter:: + + class Bar[T, S = list[T]]: ... + + Bar[int] # equivalent to Bar[int, list[int]] + +Referring to a type parameter that does not appear earlier in the same type +parameter list, including the type parameter itself, raises :exc:`TypeError` +when the default is used. + The following example indicates the full set of allowed type parameter declarations:: def overly_generic[ diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index b6588269e1d0c65..92120eb76e4a304 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -7,7 +7,8 @@ import weakref from test.support import check_syntax_error, run_code, run_no_yield_async_fn -from typing import Generic, NoDefault, Sequence, TypeAliasType, TypeVar, TypeVarTuple, ParamSpec, get_args +from typing import (Callable, Generic, NoDefault, Sequence, TypeAliasType, + TypeVar, TypeVarTuple, ParamSpec, get_args) class TypeParamsInvalidTest(unittest.TestCase): @@ -1418,6 +1419,111 @@ def test_symtable_key_regression_name(self): self.assertEqual(ns["X1"].__type_params__[0].__default__, "A") self.assertEqual(ns["X2"].__type_params__[0].__default__, "B") + def test_default_refers_to_earlier_type_param(self): + class A[T1, T2=T1]: ... + + self.assertEqual(A[int].__args__, (int, int)) + self.assertEqual(A[int, str].__args__, (int, str)) + + def test_default_refers_to_earlier_type_param_chain(self): + class A[T1, T2=T1, T3=T2]: ... + + self.assertEqual(A[int].__args__, (int, int, int)) + self.assertEqual(A[int, str].__args__, (int, str, str)) + self.assertEqual(A[int, str, bool].__args__, (int, str, bool)) + + def test_default_refers_to_earlier_type_param_nested(self): + class A[T1, T2=list[T1]]: ... + + self.assertEqual(A[int].__args__, (int, list[int])) + + class B[T1, T2, T3=dict[T1, T2]]: ... + + self.assertEqual(B[int, str].__args__, (int, str, dict[int, str])) + + class C[T1, T2, T3=T1 | T2]: ... + + self.assertEqual(C[int, str].__args__, (int, str, int | str)) + + class D[T1, T2=Callable[[T1], T1]]: ... + + self.assertEqual(D[int].__args__, (int, Callable[[int], int])) + + def test_default_refers_to_earlier_type_param_typevartuple(self): + class A[T1, *Ts=*tuple[T1, ...]]: ... + + self.assertEqual(A[int].__args__, (int, *tuple[int, ...])) + + class B[T1, T2, *Ts=*tuple[T1, T2]]: ... + + self.assertEqual(B[int, str].__args__, (int, str, int, str)) + + def test_default_refers_to_earlier_type_param_paramspec(self): + class A[T1, **P=[T1, int]]: ... + + self.assertEqual(A[str].__args__, (str, (str, int))) + + class B[**P, T=int]: ... + + self.assertEqual(B[[int, str]].__args__, ((int, str), int)) + + def test_default_refers_to_earlier_type_param_in_base_class(self): + # gh-140596: omitting a type parameter with a default when + # subclassing used to leave the default unsubstituted, which made + # the type parameter it refers to leak into the subclass. + class Bar[T, S=T]: ... + class Baz[U](Bar[U]): ... + + U, = Baz.__type_params__ + self.assertEqual(Baz.__orig_bases__[0].__args__, (U, U)) + self.assertEqual(Baz.__parameters__, (U,)) + self.assertEqual(Baz[int].__args__, (int,)) + + def test_default_refers_to_type_param_from_enclosing_scope(self): + # A default that refers to a type variable which is not a type + # parameter of the class itself is left untouched. + T = TypeVar('T') + S = TypeVar('S', default=T) + class A(Generic[S]): ... + + self.assertEqual(A[()].__args__, (T,)) + + def test_default_refers_to_type_param_supplied_by_the_user(self): + T = TypeVar('T') + class A[T1, T2=T1]: ... + + self.assertEqual(A[T].__args__, (T, T)) + + def test_default_refers_to_itself(self): + class A[T1=T1]: ... + + with self.assertRaisesRegex( + TypeError, + r"The default of type parameter T1 refers to type parameter T1, " + r"which is not declared before it", + ): + A[()] + + def test_default_refers_to_later_type_param(self): + class A[T1=T2, T2=int]: ... + + with self.assertRaisesRegex( + TypeError, + r"The default of type parameter T1 refers to type parameter T2, " + r"which is not declared before it", + ): + A[()] + + def test_defaults_refer_to_each_other(self): + class A[T1=T2, T2=T1]: ... + + with self.assertRaisesRegex( + TypeError, + r"The default of type parameter T1 refers to type parameter T2, " + r"which is not declared before it", + ): + A[()] + class TestEvaluateFunctions(unittest.TestCase): def test_general(self): diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2875303fb156197..ce3ad895f07c587 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -826,6 +826,36 @@ def test_pickle(self): self.assertEqual(z.__bound__, typevar.__bound__) self.assertEqual(z.__default__, typevar.__default__) + def test_default_referring_to_earlier_type_param(self): + T = TypeVar('T') + U = TypeVar('U', default=T) + V = TypeVar('V', default=List[U]) + + class A(Generic[T, U, V]): ... + + self.assertEqual(A[int].__args__, (int, int, List[int])) + self.assertEqual(A[int, str].__args__, (int, str, List[str])) + self.assertEqual(A[int, str, bool].__args__, (int, str, bool)) + + def test_default_referring_to_earlier_type_param_alias(self): + T = TypeVar('T') + U = TypeVar('U', default=T) + Alias = Union[T, U] + + self.assertEqual(Alias[int], int) + self.assertEqual(Alias[int, str], Union[int, str]) + + def test_default_referring_to_earlier_paramspec_and_typevartuple(self): + T = TypeVar('T') + Ts = TypeVarTuple('Ts', default=Unpack[Tuple[T, int]]) + P = ParamSpec('P', default=[T, int]) + + class A(Generic[T, Unpack[Ts]]): ... + self.assertEqual(A[str].__args__, (str, str, int)) + + class B(Generic[T, P]): ... + self.assertEqual(B[str].__args__, (str, (str, int))) + def template_replace(templates: list[str], replacements: dict[str, list[str]]) -> list[tuple[str]]: """Renders templates with possible combinations of replacements. diff --git a/Lib/typing.py b/Lib/typing.py index 65e1d1ea6be5844..c249b42e66317f9 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1090,6 +1090,62 @@ def _typevar_subst(self, arg): return arg +def _resolve_type_param_default(param, default, params, resolved): + """Substitute already-bound type parameters into a type parameter default. + + PEP 696 allows the default of a type parameter to refer to type parameters + that appear earlier in the same type parameter list, for example:: + + class A[T, S = list[T]]: ... + + `param` is the type parameter whose `default` is being filled in, `params` + is the full list of type parameters of the object being subscripted, and + `resolved` holds the values that have already been determined for the + leading ``len(resolved)`` of them, so that ``params[len(resolved)]`` is + `param` itself. + + Type parameters that come from an enclosing scope (they do not appear in + `params`) are left untouched. Referring to a type parameter that is not + bound yet is an error; that covers both self-references such as + ``class A[T = T]`` and cycles such as ``class A[T = S, S = T]``. + """ + bound = dict(zip(params, resolved)) + unbound = frozenset(params[len(resolved):]) + + def resolve(value): + if isinstance(value, (TypeVar, ParamSpec, TypeVarTuple)): + if value in bound: + return bound[value] + if value in unbound: + raise TypeError( + f"The default of type parameter {param} refers to type " + f"parameter {value}, which is not declared before it" + ) + return value + if isinstance(value, list): + return [resolve(v) for v in value] + if isinstance(value, tuple): + return tuple(resolve(v) for v in value) + subparams = getattr(value, '__parameters__', ()) + if not subparams: + return value + subargs = [] + changed = False + for subparam in subparams: + new_subarg = resolve(subparam) + changed |= new_subarg is not subparam + if (isinstance(subparam, TypeVarTuple) + and isinstance(new_subarg, tuple)): + subargs.extend(new_subarg) + else: + subargs.append(new_subarg) + if not changed: + return value + return value[tuple(subargs)] + + return resolve(default) + + def _typevartuple_prepare_subst(self, alias, args): params = alias.__parameters__ typevartuple_index = params.index(self) @@ -1118,7 +1174,9 @@ def _typevartuple_prepare_subst(self, alias, args): raise TypeError(f"Too few arguments for {alias};" f" actual {alen}, expected at least {plen-1}") if left == alen - right and self.has_default(): - replacement = _unpack_args(self.__default__) + default = _resolve_type_param_default(self, self.__default__, params, + args[:left]) + replacement = _unpack_args(default) else: replacement = args[left: alen - right] @@ -1144,7 +1202,9 @@ def _paramspec_prepare_subst(self, alias, args): params = alias.__parameters__ i = params.index(self) if i == len(args) and self.has_default(): - args = (*args, self.__default__) + default = _resolve_type_param_default(self, self.__default__, params, + args) + args = (*args, default) if i >= len(args): raise TypeError(f"Too few arguments for {alias}") # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612. diff --git a/Misc/NEWS.d/next/Library/2026-08-13-11-42-08.gh-issue-140596.Kq3vLt.rst b/Misc/NEWS.d/next/Library/2026-08-13-11-42-08.gh-issue-140596.Kq3vLt.rst new file mode 100644 index 000000000000000..0074470d5e61512 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-13-11-42-08.gh-issue-140596.Kq3vLt.rst @@ -0,0 +1,6 @@ +Fix substitution of :pep:`696` type parameter defaults that refer to earlier +type parameters in the same type parameter list. ``class Bar[T, S = T]`` now +resolves ``Bar[int]`` to ``Bar[int, int]`` instead of leaving ``S`` bound to +the unsubstituted ``T``, which previously made ``class Baz[U](Bar[U])`` raise +:exc:`TypeError`. A default that refers to a type parameter which is not +declared before it now raises :exc:`TypeError` when it is used. diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index b2c3c79c93ff195..94345eca3499aa0 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -807,8 +807,19 @@ typevar_typing_prepare_subst_impl(typevarobject *self, PyObject *alias, return NULL; } if (dflt != &_Py_NoDefaultStruct) { - PyObject *new_args = PyTuple_Pack(1, dflt); + // The default may refer to type parameters that appear earlier in + // the same type parameter list; those are already resolved in + // "args", so substitute them in. + PyObject *resolve_args[4] = {(PyObject *)self, dflt, params, args}; + PyObject *resolved = call_typing_func_object( + "_resolve_type_param_default", resolve_args, 4); Py_DECREF(dflt); + if (resolved == NULL) { + Py_DECREF(params); + return NULL; + } + PyObject *new_args = PyTuple_Pack(1, resolved); + Py_DECREF(resolved); if (new_args == NULL) { Py_DECREF(params); return NULL;