From b1013fe5eeff45dde61f8024d98ef5f2f6bfc37a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 21 Oct 2025 06:47:30 -0400 Subject: [PATCH 1/5] Fix memory leak upon __hash__ returning a non-integer. --- Objects/typeobject.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 29233c1959c4d0..721d48712446a0 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -10569,6 +10569,7 @@ slot_tp_hash(PyObject *self) return PyObject_HashNotImplemented(self); } if (!PyLong_Check(res)) { + Py_DECREF(res); PyErr_SetString(PyExc_TypeError, "__hash__ method should return an integer"); return -1; From ed017f0773bf3a80a86d96b88ddc3e766f5dd241 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 21 Oct 2025 06:50:38 -0400 Subject: [PATCH 2/5] Add a test case. --- Lib/test/test_hash.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_hash.py b/Lib/test/test_hash.py index cf9db66a29ae11..8097178506e9f4 100644 --- a/Lib/test/test_hash.py +++ b/Lib/test/test_hash.py @@ -166,6 +166,17 @@ def test_hashes(self): for obj in self.hashes_to_check: self.assertEqual(hash(obj), _default_hash(obj)) + def test_invalid_hash_typeerror(self): + # GH-140406: The returned object from __hash__() would leak if it + # wasn't an integer. + class A: + def __hash__(self): + return 1.0 + + for _ in range(100): + with self.assertRaises(TypeError): + hash(A()) + class HashRandomizationTests: # Each subclass should define a field "repr_", containing the repr() of From 9f89372308f197a7c98a00ef27a5c096bc436f34 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 21 Oct 2025 06:51:53 -0400 Subject: [PATCH 3/5] Add blurb. --- .../2025-10-21-06-51-50.gh-issue-140406.0gJs8M.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-21-06-51-50.gh-issue-140406.0gJs8M.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-21-06-51-50.gh-issue-140406.0gJs8M.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-21-06-51-50.gh-issue-140406.0gJs8M.rst new file mode 100644 index 00000000000000..3506ba42581faa --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-21-06-51-50.gh-issue-140406.0gJs8M.rst @@ -0,0 +1,2 @@ +Fix memory leak when an object's :meth:`~object.__hash__` method returns an +object that isn't an :class:`int`. From 1b96db7eefef5988af3e46c67c0ff15901150257 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 21 Oct 2025 06:58:43 -0400 Subject: [PATCH 4/5] Remove unnecessary loop. --- Lib/test/test_hash.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_hash.py b/Lib/test/test_hash.py index 8097178506e9f4..9dec248550a34d 100644 --- a/Lib/test/test_hash.py +++ b/Lib/test/test_hash.py @@ -173,9 +173,8 @@ class A: def __hash__(self): return 1.0 - for _ in range(100): - with self.assertRaises(TypeError): - hash(A()) + with self.assertRaises(TypeError): + hash(A()) class HashRandomizationTests: From 5754822bce07ffb12dd9e4db5742ac69b21becb1 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 21 Oct 2025 07:08:14 -0400 Subject: [PATCH 5/5] Move test to test_builtin. --- Lib/test/test_builtin.py | 10 ++++++++++ Lib/test/test_hash.py | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 85cfe5c90f48af..034cd5f7f9e89f 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -1184,6 +1184,16 @@ def __hash__(self): return self self.assertEqual(hash(Z(42)), hash(42)) + def test_invalid_hash_typeerror(self): + # GH-140406: The returned object from __hash__() would leak if it + # wasn't an integer. + class A: + def __hash__(self): + return 1.0 + + with self.assertRaises(TypeError): + hash(A()) + def test_hex(self): self.assertEqual(hex(16), '0x10') self.assertEqual(hex(-16), '-0x10') diff --git a/Lib/test/test_hash.py b/Lib/test/test_hash.py index 9dec248550a34d..cf9db66a29ae11 100644 --- a/Lib/test/test_hash.py +++ b/Lib/test/test_hash.py @@ -166,16 +166,6 @@ def test_hashes(self): for obj in self.hashes_to_check: self.assertEqual(hash(obj), _default_hash(obj)) - def test_invalid_hash_typeerror(self): - # GH-140406: The returned object from __hash__() would leak if it - # wasn't an integer. - class A: - def __hash__(self): - return 1.0 - - with self.assertRaises(TypeError): - hash(A()) - class HashRandomizationTests: # Each subclass should define a field "repr_", containing the repr() of