From a4a697620bc8bbcae717ff842f007c633fa59d95 Mon Sep 17 00:00:00 2001 From: Builder106 Date: Sat, 20 Jun 2026 09:41:21 -0700 Subject: [PATCH] gh-140665: Substitute PEP 696 defaults referencing other parameters A type parameter's default may reference an earlier parameter in the same scope (e.g. class C[T, S = T], or S = list[T]), but the default was inserted unsubstituted when a trailing parameter was filled from it. Parametrizing C[int] produced C[int, T] instead of C[int, int], leaking the unbound type variable; nested defaults (S = list[T]) and chained defaults (S = T, U = S) were left unbound too. Add _resolve_parameter_defaults, called from _generic_class_getitem and _GenericAlias._determine_new_args, which substitutes the now-bound parameters into any argument supplied by a default. Detection is gated on the argument tuple growing during __typing_prepare_subst__, so explicitly-passed arguments such as C[int, T] are left untouched. --- Lib/test/test_typing.py | 30 ++++++++++++ Lib/typing.py | 48 +++++++++++++++++++ ...-06-20-09-34-27.gh-issue-140665.398faA.rst | 5 ++ 3 files changed, 83 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-20-09-34-27.gh-issue-140665.398faA.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 042604ed7c1a423..0dd0e548a739254 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 1579f492003f748..5162a35af968f56 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 000000000000000..742e41ecf0776c1 --- /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.