diff --git a/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py index 05bc6a80888a067..d80a7fbfd18796d 100644 --- a/Lib/test/test_type_aliases.py +++ b/Lib/test/test_type_aliases.py @@ -1,7 +1,8 @@ import pickle import types import unittest -from test.support import check_syntax_error, run_code +import weakref +from test.support import check_syntax_error, gc_collect, run_code from test.typinganndata import mod_generics_cache from typing import ( @@ -131,6 +132,24 @@ def more_generic[T, *Ts, **P](): T, Ts, P = more_generic.__type_params__ self.assertEqual(alias.__value__, tuple[Callable[P, tuple[T, *Ts]], Callable[P2, tuple[T2, *Ts2]]]) + def test_reentrant_value_does_not_leak(self): + class Value: + pass + + refs = [] + + def compute(): + value = Value() + refs.append(weakref.ref(value)) + if len(refs) == 1: + Alias.__value__ + return value + + type Alias = compute() + self.assertIs(Alias.__value__, refs[0]()) + gc_collect() + self.assertIsNone(refs[1]()) + def test_subscripting(self): type NonGeneric = int type Generic[A] = dict[A, A] diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.tali01.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.tali01.rst new file mode 100644 index 000000000000000..a4f672fed87cfe7 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.tali01.rst @@ -0,0 +1,2 @@ +Fix a reference leak when the value of a type alias is evaluated +recursively or concurrently. diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 398a63257beb4a2..11ff8320fd9a487 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -1899,14 +1899,23 @@ typealias_dealloc(PyObject *self) static PyObject * typealias_get_value(typealiasobject *ta) { - if (ta->value != NULL) { - return Py_NewRef(ta->value); + PyObject *result; + Py_BEGIN_CRITICAL_SECTION(ta); + result = Py_XNewRef(ta->value); + Py_END_CRITICAL_SECTION(); + if (result != NULL) { + return result; } - PyObject *result = PyObject_CallNoArgs(ta->compute_value); + result = PyObject_CallNoArgs(ta->compute_value); if (result == NULL) { return NULL; } + PyObject *old_value; + Py_BEGIN_CRITICAL_SECTION(ta); + old_value = ta->value; ta->value = Py_NewRef(result); + Py_END_CRITICAL_SECTION(); + Py_XDECREF(old_value); return result; }