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;