https://github.com/python/cpython/commit/59d3594ca12939dea0a537d9964d8d637546855c
commit: 59d3594ca12939dea0a537d9964d8d637546855c
branch: main
author: Bartosz Sławecki <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-01-18T21:29:11-08:00
summary:
gh-143831: Compare cells by identity in forward references (#143848)
files:
A Misc/NEWS.d/next/Library/2026-01-16-06-22-10.gh-issue-143831.VLBTLp.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index 4085cc6bef7954..832d160de7f4e5 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -279,7 +279,13 @@ def __eq__(self, other):
# because dictionaries are not hashable.
and self.__globals__ is other.__globals__
and self.__forward_is_class__ == other.__forward_is_class__
- and self.__cell__ == other.__cell__
+ # Two separate cells are always considered unequal in forward refs.
+ and (
+ {name: id(cell) for name, cell in self.__cell__.items()}
+ == {name: id(cell) for name, cell in other.__cell__.items()}
+ if isinstance(self.__cell__, dict) and
isinstance(other.__cell__, dict)
+ else self.__cell__ is other.__cell__
+ )
and self.__owner__ == other.__owner__
and (
(tuple(sorted(self.__extra_names__.items())) if
self.__extra_names__ else None) ==
@@ -293,7 +299,10 @@ def __hash__(self):
self.__forward_module__,
id(self.__globals__), # dictionaries are not hashable, so hash by
identity
self.__forward_is_class__,
- tuple(sorted(self.__cell__.items())) if isinstance(self.__cell__,
dict) else self.__cell__,
+ ( # cells are not hashable as well
+ tuple(sorted([(name, id(cell)) for name, cell in
self.__cell__.items()]))
+ if isinstance(self.__cell__, dict) else id(self.__cell__),
+ ),
self.__owner__,
tuple(sorted(self.__extra_names__.items())) if
self.__extra_names__ else None,
))
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index a8537871d294cf..6b75da32fa944a 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -8,6 +8,7 @@
import itertools
import pickle
from string.templatelib import Template, Interpolation
+import types
import typing
import sys
import unittest
@@ -1862,6 +1863,39 @@ def foo(a: c1_gth, b: c2_gth):
self.assertNotEqual(hash(c3), hash(c4))
self.assertEqual(hash(c3), hash(ForwardRef("int", module=__name__)))
+ def test_forward_equality_and_hash_with_cells(self):
+ """Regression test for GH-143831."""
+ class A:
+ def one(_) -> C1:
+ """One cell."""
+
+ one_f = ForwardRef("C1", owner=one)
+ one_f_ga1 = get_annotations(one,
format=Format.FORWARDREF)["return"]
+ one_f_ga2 = get_annotations(one,
format=Format.FORWARDREF)["return"]
+ self.assertIsInstance(one_f_ga1.__cell__, types.CellType)
+ self.assertIs(one_f_ga1.__cell__, one_f_ga2.__cell__)
+
+ def two(_) -> C1 | C2:
+ """Two cells."""
+
+ two_f_ga1 = get_annotations(two,
format=Format.FORWARDREF)["return"]
+ two_f_ga2 = get_annotations(two,
format=Format.FORWARDREF)["return"]
+ self.assertIsNot(two_f_ga1.__cell__, two_f_ga2.__cell__)
+ self.assertIsInstance(two_f_ga1.__cell__, dict)
+ self.assertIsInstance(two_f_ga2.__cell__, dict)
+
+ type C1 = None
+ type C2 = None
+
+ self.assertNotEqual(A.one_f, A.one_f_ga1)
+ self.assertNotEqual(hash(A.one_f), hash(A.one_f_ga1))
+
+ self.assertEqual(A.one_f_ga1, A.one_f_ga2)
+ self.assertEqual(hash(A.one_f_ga1), hash(A.one_f_ga2))
+
+ self.assertEqual(A.two_f_ga1, A.two_f_ga2)
+ self.assertEqual(hash(A.two_f_ga1), hash(A.two_f_ga2))
+
def test_forward_equality_namespace(self):
def namespace1():
a = ForwardRef("A")
diff --git
a/Misc/NEWS.d/next/Library/2026-01-16-06-22-10.gh-issue-143831.VLBTLp.rst
b/Misc/NEWS.d/next/Library/2026-01-16-06-22-10.gh-issue-143831.VLBTLp.rst
new file mode 100644
index 00000000000000..620adea1b6d782
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-01-16-06-22-10.gh-issue-143831.VLBTLp.rst
@@ -0,0 +1,3 @@
+:class:`annotationlib.ForwardRef` objects are now hashable when created from
+annotation scopes with closures. Previously, hashing such objects would
+throw an exception. Patch by Bartosz Sławecki.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]