From 1cf527f8229ec6b93e26d19f2dd60b8962c4e948 Mon Sep 17 00:00:00 2001 From: iliev-blnk Date: Wed, 28 Oct 2026 22:16:06 +0200 Subject: [PATCH] gh-160788: Fix exponential time in code object comparison code_richcompare() compared the constant key of the whole co_consts tuple. The key of a tuple is a pair of the item keys and the tuple itself, so each nested code object was compared twice per level of nesting. Compare the keys of the constants one by one instead. --- Lib/test/test_code.py | 19 +++++++++++ ...-10-28-20-07-09.gh-issue-160788.0mN49z.rst | 2 ++ Objects/codeobject.c | 32 +++++++++++-------- 3 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-20-07-09.gh-issue-160788.0mN49z.rst diff --git a/Lib/test/test_code.py b/Lib/test/test_code.py index f54703cf37b2de5..6be2e471d1744e7 100644 --- a/Lib/test/test_code.py +++ b/Lib/test/test_code.py @@ -630,6 +630,25 @@ def test_code_hash_uses_bytecode(self): self.assertNotEqual(c, c1) self.assertNotEqual(hash(c), hash(c1)) + def test_code_equality_uses_consts(self): + c = compile("0.0", "example.py", "eval") + self.assertEqual(c.co_consts, (0.0,)) + self.assertEqual(c, c.replace(co_consts=(0.0,))) + for consts in [(-0.0,), (0,), (False,), ((0.0,),), (0.0, None)]: + with self.subTest(consts=consts): + self.assertNotEqual(c, c.replace(co_consts=consts)) + + def test_code_equality_nested(self): + # gh-160788: this took time exponential in the nesting depth. + depth = 30 + source = "".join(" " * i + f"class C{i}:\n" for i in range(depth)) + indent = " " * depth + code1 = compile(source + indent + "pass\n", "example.py", "exec") + code2 = compile(source + indent + "pass\n", "example.py", "exec") + code3 = compile(source + indent + "x = 1\n", "example.py", "exec") + self.assertEqual(code1, code2) + self.assertNotEqual(code1, code3) + @cpython_only def test_code_equal_with_instrumentation(self): """ GH-109052 diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-20-07-09.gh-issue-160788.0mN49z.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-20-07-09.gh-issue-160788.0mN49z.rst new file mode 100644 index 000000000000000..61c3ae662c4923d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-28-20-07-09.gh-issue-160788.0mN49z.rst @@ -0,0 +1,2 @@ +Comparing :ref:`code objects ` with ``==`` no longer takes +time exponential in the nesting depth of their classes and functions. diff --git a/Objects/codeobject.c b/Objects/codeobject.c index eb390ab24ec13e6..a45f9e7a85a868d 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -2500,7 +2500,7 @@ code_richcompare(PyObject *self, PyObject *other, int op) { PyCodeObject *co, *cp; int eq; - PyObject *consts1, *consts2; + PyObject *key1, *key2; PyObject *res; if ((op != Py_EQ && op != Py_NE) || @@ -2537,19 +2537,25 @@ code_richcompare(PyObject *self, PyObject *other, int op) i += _PyOpcode_Caches[co_instr.op.code]; } - /* compare constants */ - consts1 = _PyCode_ConstantKey(co->co_consts); - if (!consts1) - return NULL; - consts2 = _PyCode_ConstantKey(cp->co_consts); - if (!consts2) { - Py_DECREF(consts1); - return NULL; + /* compare constants one by one: the key of the whole tuple also holds + the tuple itself, so comparing it would compare nested code objects + twice, which takes exponential time in the nesting depth */ + eq = PyTuple_GET_SIZE(co->co_consts) == PyTuple_GET_SIZE(cp->co_consts); + if (!eq) goto unequal; + for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(co->co_consts); i++) { + key1 = _PyCode_ConstantKey(PyTuple_GET_ITEM(co->co_consts, i)); + if (!key1) + return NULL; + key2 = _PyCode_ConstantKey(PyTuple_GET_ITEM(cp->co_consts, i)); + if (!key2) { + Py_DECREF(key1); + return NULL; + } + eq = PyObject_RichCompareBool(key1, key2, Py_EQ); + Py_DECREF(key1); + Py_DECREF(key2); + if (eq <= 0) goto unequal; } - eq = PyObject_RichCompareBool(consts1, consts2, Py_EQ); - Py_DECREF(consts1); - Py_DECREF(consts2); - if (eq <= 0) goto unequal; eq = PyObject_RichCompareBool(co->co_names, cp->co_names, Py_EQ); if (eq <= 0) goto unequal;