From 201b53247969160b5bbb9669b1ccb38f6966abe5 Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Thu, 25 Jun 2026 22:32:23 +0800 Subject: [PATCH 1/4] gh-144361: Fix NameError when type parameter default refers to a forward name --- Lib/typing.py | 21 ++++++++++++++++--- ...-06-25-22-30-00.gh-issue-144361.lL9mN2.rst | 4 ++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-25-22-30-00.gh-issue-144361.lL9mN2.rst diff --git a/Lib/typing.py b/Lib/typing.py index 1579f492003f748..fb4c7af4e06f950 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1099,7 +1099,12 @@ 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__) + try: + default = self.__default__ + except NameError: + default = annotationlib.call_evaluate_function( + self.evaluate_default, annotationlib.Format.FORWARDREF) + replacement = _unpack_args(default) else: replacement = args[left: alen - right] @@ -1125,7 +1130,12 @@ 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__) + try: + default = self.__default__ + except NameError: + default = annotationlib.call_evaluate_function( + self.evaluate_default, annotationlib.Format.FORWARDREF) + 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. @@ -1185,7 +1195,12 @@ def _generic_class_getitem(cls, args): for param in parameters: prepare = getattr(param, '__typing_prepare_subst__', None) if prepare is not None: - args = prepare(cls, args) + try: + args = prepare(cls, args) + except NameError: + default = annotationlib.call_evaluate_function( + param.evaluate_default, annotationlib.Format.FORWARDREF) + args = (*args, default) _check_generic_specialization(cls, args) new_args = [] diff --git a/Misc/NEWS.d/next/Library/2026-06-25-22-30-00.gh-issue-144361.lL9mN2.rst b/Misc/NEWS.d/next/Library/2026-06-25-22-30-00.gh-issue-144361.lL9mN2.rst new file mode 100644 index 000000000000000..a791b363ac32d42 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-25-22-30-00.gh-issue-144361.lL9mN2.rst @@ -0,0 +1,4 @@ +Fix :exc:`NameError` when a type parameter default (PEP 696/PEP 749) +refers to a name defined later in the module. Now three call sites in +:mod:`typing` fall back to :func:`annotationlib.call_evaluate_function` +with :data:`~annotationlib.Format.FORWARDREF` when eager evaluation fails. From 2a0ffacf4175f850c64fb9a53581d90eacd5d4a0 Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Fri, 26 Jun 2026 09:46:59 +0800 Subject: [PATCH 2/4] gh-144361: Add tests for forward reference default in type parameters --- Lib/test/test_typing.py | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ed07503cd63f12b..740d32bfedd8fa7 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -823,6 +823,54 @@ def test_pickle(self): self.assertEqual(z.__bound__, typevar.__bound__) self.assertEqual(z.__default__, typevar.__default__) + def test_forward_reference_default_typevar(self): + ns = run_code( + """ + class A[T, U = ForwardName]: + pass + """ + ) + U, A = ns["A"].__type_params__[1], ns["A"] + with self.assertRaises(NameError): + U.__default__ + result = A[int] + self.assertIsInstance(result.__args__[0], type) + self.assertIs(int, result.__args__[0]) + self.assertIsInstance(result.__args__[1], ForwardRef) + self.assertEqual(result.__args__[1].__forward_arg__, 'ForwardName') + + def test_forward_reference_default_typevartuple(self): + ns = run_code( + """ + class A[T, *Ts = ForwardName]: + pass + """ + ) + Ts, A = ns["A"].__type_params__[1], ns["A"] + with self.assertRaises(NameError): + Ts.__default__ + result = A[int] + self.assertIsInstance(result.__args__[0], type) + self.assertIs(int, result.__args__[0]) + self.assertIsInstance(result.__args__[1], ForwardRef) + self.assertEqual(result.__args__[1].__forward_arg__, 'ForwardName') + + def test_forward_reference_default_paramspec(self): + ns = run_code( + """ + class A[T, **P = ForwardName]: + pass + """ + ) + P, A = ns["A"].__type_params__[1], ns["A"] + with self.assertRaises(NameError): + P.__default__ + result = A[int] + self.assertIsInstance(result.__args__[0], type) + self.assertIs(int, result.__args__[0]) + self.assertIsInstance(result.__args__[1], ForwardRef) + self.assertEqual(result.__args__[1].__forward_arg__, 'ForwardName') + def template_replace(templates: list[str], replacements: dict[str, list[str]]) -> list[tuple[str]]: """Renders templates with possible combinations of replacements. From f1da15dea934713a7c00c1753bf1406c8bf47ebc Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Thu, 9 Jul 2026 20:43:28 +0800 Subject: [PATCH 3/4] gh-144361: Handle forward reference default in TypeVar C code Instead of catching NameError in Python's _generic_class_getitem, fix the C implementation of TypeVar.__typing_prepare_subst__ to fall back to FORWARDREF format when typevar_default() raises NameError. Co-authored-by: Jelle Zijlstra --- Lib/typing.py | 7 +------ Objects/typevarobject.c | 43 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index fb4c7af4e06f950..3ba481770618207 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1195,12 +1195,7 @@ def _generic_class_getitem(cls, args): for param in parameters: prepare = getattr(param, '__typing_prepare_subst__', None) if prepare is not None: - try: - args = prepare(cls, args) - except NameError: - default = annotationlib.call_evaluate_function( - param.evaluate_default, annotationlib.Format.FORWARDREF) - args = (*args, default) + args = prepare(cls, args) _check_generic_specialization(cls, args) new_args = [] diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index b2c3c79c93ff195..6ddce6ca4506820 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -350,6 +350,24 @@ call_typing_func_object(const char *name, PyObject **args, size_t nargs) return result; } +static PyObject * +call_annotationlib_func_object(const char *name, PyObject **args, size_t nargs) +{ + PyObject *annotationlib = PyImport_ImportModule("annotationlib"); + if (annotationlib == NULL) { + return NULL; + } + PyObject *func = PyObject_GetAttrString(annotationlib, name); + if (func == NULL) { + Py_DECREF(annotationlib); + return NULL; + } + PyObject *result = PyObject_Vectorcall(func, args, nargs, NULL); + Py_DECREF(func); + Py_DECREF(annotationlib); + return result; +} + static PyObject * type_check(PyObject *arg, const char *msg) { @@ -803,8 +821,29 @@ typevar_typing_prepare_subst_impl(typevarobject *self, PyObject *alias, // If the TypeVar has a default, use it. PyObject *dflt = typevar_default((PyObject *)self, NULL); if (dflt == NULL) { - Py_DECREF(params); - return NULL; + if (!PyErr_ExceptionMatches(PyExc_NameError)) { + Py_DECREF(params); + return NULL; + } + PyErr_Clear(); + if (self->evaluate_default == NULL) { + dflt = &_Py_NoDefaultStruct; + } + else { + PyObject *format = PyLong_FromLong(_Py_ANNOTATE_FORMAT_FORWARDREF); + if (format == NULL) { + Py_DECREF(params); + return NULL; + } + PyObject *call_args[2] = {self->evaluate_default, format}; + dflt = call_annotationlib_func_object( + "call_evaluate_function", call_args, 2); + Py_DECREF(format); + if (dflt == NULL) { + Py_DECREF(params); + return NULL; + } + } } if (dflt != &_Py_NoDefaultStruct) { PyObject *new_args = PyTuple_Pack(1, dflt); From 774870918d1ec59b85e917e00fab7bf2d56e5d78 Mon Sep 17 00:00:00 2001 From: tangyuan0821 Date: Fri, 14 Aug 2026 11:29:06 +0800 Subject: [PATCH 4/4] gh-144361: Move forward-reference default fallback to Python Rework the type parameter default fallback so the Format.FORWARDREF logic lives in typing.py instead of being hardcoded in the C code. TypeVar.__typing_prepare_subst__ now delegates to a new _typevar_prepare_subst function in typing.py, mirroring how TypeVarTuple and ParamSpec already delegate to Python. All three share a single _typeparam_default helper that falls back to annotationlib.call_evaluate_function with Format.FORWARDREF when eager evaluation raises NameError. --- Lib/typing.py | 41 ++++++++++++++------ Objects/typevarobject.c | 85 +---------------------------------------- 2 files changed, 31 insertions(+), 95 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 3ba481770618207..d068b8607a9e137 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1071,6 +1071,33 @@ def _typevar_subst(self, arg): return arg +def _typeparam_default(tp): + """Return the default value of a type parameter. + + If the default refers to a name that is not yet defined, return the + corresponding forward reference instead of raising NameError. + """ + try: + return tp.__default__ + except NameError: + return annotationlib.call_evaluate_function( + tp.evaluate_default, annotationlib.Format.FORWARDREF) + + +def _typevar_prepare_subst(self, alias, args): + params = alias.__parameters__ + i = params.index(self) + alen = len(args) + if i < alen: + # We already have a value for this TypeVar + return args + if i == alen and self.has_default(): + # If the TypeVar has a default, use it. + return (*args, _typeparam_default(self)) + raise TypeError(f"Too few arguments for {alias};" + f" actual {alen}, expected at least {i + 1}") + + def _typevartuple_prepare_subst(self, alias, args): params = alias.__parameters__ typevartuple_index = params.index(self) @@ -1099,12 +1126,7 @@ 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(): - try: - default = self.__default__ - except NameError: - default = annotationlib.call_evaluate_function( - self.evaluate_default, annotationlib.Format.FORWARDREF) - replacement = _unpack_args(default) + replacement = _unpack_args(_typeparam_default(self)) else: replacement = args[left: alen - right] @@ -1130,12 +1152,7 @@ def _paramspec_prepare_subst(self, alias, args): params = alias.__parameters__ i = params.index(self) if i == len(args) and self.has_default(): - try: - default = self.__default__ - except NameError: - default = annotationlib.call_evaluate_function( - self.evaluate_default, annotationlib.Format.FORWARDREF) - args = (*args, default) + args = (*args, _typeparam_default(self)) 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/Objects/typevarobject.c b/Objects/typevarobject.c index 6ddce6ca4506820..b1533a4636287a9 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -350,24 +350,6 @@ call_typing_func_object(const char *name, PyObject **args, size_t nargs) return result; } -static PyObject * -call_annotationlib_func_object(const char *name, PyObject **args, size_t nargs) -{ - PyObject *annotationlib = PyImport_ImportModule("annotationlib"); - if (annotationlib == NULL) { - return NULL; - } - PyObject *func = PyObject_GetAttrString(annotationlib, name); - if (func == NULL) { - Py_DECREF(annotationlib); - return NULL; - } - PyObject *result = PyObject_Vectorcall(func, args, nargs, NULL); - Py_DECREF(func); - Py_DECREF(annotationlib); - return result; -} - static PyObject * type_check(PyObject *arg, const char *msg) { @@ -798,71 +780,8 @@ typevar_typing_prepare_subst_impl(typevarobject *self, PyObject *alias, PyObject *args) /*[clinic end generated code: output=82c3f4691e0ded22 input=201a750415d14ffb]*/ { - PyObject *params = PyObject_GetAttrString(alias, "__parameters__"); - if (params == NULL) { - return NULL; - } - Py_ssize_t i = PySequence_Index(params, (PyObject *)self); - if (i == -1) { - Py_DECREF(params); - return NULL; - } - Py_ssize_t args_len = PySequence_Length(args); - if (args_len == -1) { - Py_DECREF(params); - return NULL; - } - if (i < args_len) { - // We already have a value for our TypeVar - Py_DECREF(params); - return Py_NewRef(args); - } - else if (i == args_len) { - // If the TypeVar has a default, use it. - PyObject *dflt = typevar_default((PyObject *)self, NULL); - if (dflt == NULL) { - if (!PyErr_ExceptionMatches(PyExc_NameError)) { - Py_DECREF(params); - return NULL; - } - PyErr_Clear(); - if (self->evaluate_default == NULL) { - dflt = &_Py_NoDefaultStruct; - } - else { - PyObject *format = PyLong_FromLong(_Py_ANNOTATE_FORMAT_FORWARDREF); - if (format == NULL) { - Py_DECREF(params); - return NULL; - } - PyObject *call_args[2] = {self->evaluate_default, format}; - dflt = call_annotationlib_func_object( - "call_evaluate_function", call_args, 2); - Py_DECREF(format); - if (dflt == NULL) { - Py_DECREF(params); - return NULL; - } - } - } - if (dflt != &_Py_NoDefaultStruct) { - PyObject *new_args = PyTuple_Pack(1, dflt); - Py_DECREF(dflt); - if (new_args == NULL) { - Py_DECREF(params); - return NULL; - } - PyObject *result = PySequence_Concat(args, new_args); - Py_DECREF(params); - Py_DECREF(new_args); - return result; - } - } - Py_DECREF(params); - PyErr_Format(PyExc_TypeError, - "Too few arguments for %S; actual %zd, expected at least %zd", - alias, args_len, i + 1); - return NULL; + PyObject *args_array[3] = {(PyObject *)self, alias, args}; + return call_typing_func_object("_typevar_prepare_subst", args_array, 3); } /*[clinic input]