diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 042604ed7c1a42..0dd0e548a73925 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -706,6 +706,36 @@ class A(Generic[T, U, Unpack[Ts]]): ... self.assertEqual(A[int, str, range].__args__, (int, str, range)) self.assertEqual(A[int, str, *tuple[int, ...]].__args__, (int, str, *tuple[int, ...])) + def test_typevar_default_referencing_other_typevar(self): + # gh-140665: a type parameter default that references an earlier type + # parameter is substituted when the generic is parameterized. + T = TypeVar("T") + S = TypeVar("S", default=T) + class A(Generic[T, S]): ... + self.assertEqual(A[int].__args__, (int, int)) + self.assertEqual(A[str].__args__, (str, str)) + # an explicit argument is not overridden by the default + self.assertEqual(A[int, str].__args__, (int, str)) + + # PEP 695 syntax + class B[T, S = T]: ... + self.assertEqual(B[int].__args__, (int, int)) + self.assertEqual(B[int, str].__args__, (int, str)) + + # a default that contains an earlier type parameter + class C[T, S = list[T]]: ... + self.assertEqual(C[int].__args__, (int, list[int])) + self.assertEqual(C[int, str].__args__, (int, str)) + + # chained defaults + class D[T, S = T, U = S]: ... + self.assertEqual(D[int].__args__, (int, int, int)) + self.assertEqual(D[int, str].__args__, (int, str, str)) + + # a concrete default is unaffected + class E[T, S = int]: ... + self.assertEqual(E[str].__args__, (str, int)) + def test_no_default_after_typevar_tuple(self): T = TypeVar("T", default=int) Ts = TypeVarTuple("Ts") diff --git a/Lib/typing.py b/Lib/typing.py index 1579f492003f74..5162a35af968f5 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1138,6 +1138,40 @@ def _paramspec_prepare_subst(self, alias, args): return args +def _resolve_parameter_defaults(parameters, args, defaulted): + """Substitute now-bound parameters into PEP 696 defaults (gh-140665). + + A type parameter's default may reference earlier parameters in the same + scope, e.g. ``class C[T, S = T]`` or ``class C[T, S = list[T]]``. Such a + default is stored unsubstituted, so once every parameter is bound to an + argument we replace those references here. *defaulted* is the set of + parameters whose argument came from their default (not from an explicit + argument). *args* is positional with *parameters*; a new tuple is returned. + """ + arg_by_param = dict(zip(parameters, args)) + for param in parameters: + if param not in defaulted: + continue + default = arg_by_param[param] + if isinstance(default, type): + continue + if getattr(default, '__typing_subst__', None) is not None: + # The default is itself a parameter, e.g. ``S = T``. + arg_by_param[param] = arg_by_param.get(default, default) + else: + subparams = getattr(default, '__parameters__', ()) + if subparams: + # The default contains parameters, e.g. ``S = list[T]``. + subargs = [] + for x in subparams: + if isinstance(x, TypeVarTuple): + subargs.extend(arg_by_param[x]) + else: + subargs.append(arg_by_param.get(x, x)) + arg_by_param[param] = default[tuple(subargs)] + return tuple(arg_by_param[p] for p in parameters) + + @_tp_cache def _generic_class_getitem(cls, args): """Parameterizes a generic class. @@ -1182,11 +1216,18 @@ def _generic_class_getitem(cls, args): f"calling 'super().__init_subclass__()'" ) raise + defaulted = set() for param in parameters: prepare = getattr(param, '__typing_prepare_subst__', None) if prepare is not None: + prev_len = len(args) args = prepare(cls, args) + if (len(args) > prev_len and isinstance(param, TypeVar) + and param.has_default()): + defaulted.add(param) _check_generic_specialization(cls, args) + if defaulted: + args = _resolve_parameter_defaults(parameters, args, defaulted) new_args = [] for param, new_arg in zip(parameters, args): @@ -1450,15 +1491,22 @@ class A(Generic[T1, T2]): pass """ params = self.__parameters__ # In the example above, this would be {T3: str} + defaulted = set() for param in params: prepare = getattr(param, '__typing_prepare_subst__', None) if prepare is not None: + prev_len = len(args) args = prepare(self, args) + if (len(args) > prev_len and isinstance(param, TypeVar) + and param.has_default()): + defaulted.add(param) alen = len(args) plen = len(params) if alen != plen: raise TypeError(f"Too {'many' if alen > plen else 'few'} arguments for {self};" f" actual {alen}, expected {plen}") + if defaulted: + args = _resolve_parameter_defaults(params, args, defaulted) new_arg_by_param = dict(zip(params, args)) return tuple(self._make_substitution(self.__args__, new_arg_by_param)) diff --git a/Misc/NEWS.d/next/Library/2026-06-20-09-34-27.gh-issue-140665.398faA.rst b/Misc/NEWS.d/next/Library/2026-06-20-09-34-27.gh-issue-140665.398faA.rst new file mode 100644 index 00000000000000..742e41ecf0776c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-20-09-34-27.gh-issue-140665.398faA.rst @@ -0,0 +1,5 @@ +Fix runtime substitution of :pep:`696` type parameter defaults that reference +earlier type parameters. Parameterizing, for example, ``class C[T, S = T]`` +as ``C[int]`` now yields ``C[int, int]`` (and ``S = list[T]`` yields +``list[int]``) instead of leaving the default unsubstituted. Patch by Olayinka +Vaughan.