GeezerVison is showing github.com — Python 3.14.1+ annotationlib regression: ForwardRef.__hash__ raises `TypeError: unhashable type: 'cell'` · Issue #143831 · python/cpython
Python 3.14.1+ annotationlib regression: ForwardRef.__hash__ raises TypeError: unhashable type: 'cell' #143831DescriptionBug report
Bug description:
This issue was originally reported in the beartype library (Ref: beartype/beartype#610), but investigation suggests it may stem from a change in Python’s annotationlib.ForwardRef implementation.
Starting with Python 3.14.1, deferred annotations (PEP 649) involving self-referential return type is raising: TypeError: unhashable type: 'cell' at decoration time. This worked correctly in Python 3.14.0 and the issue appears to persist in 3.14.2, suggesting a regression.
The error originates from annotationlib.ForwardRef.__hash__, which attempts to hash internal state containing non-hashable cell objects. Although observed via beartype, this may affects any downstream library that hashes ForwardRef objects. The following code seems to be raising the issue:
tuple(sorted(self.__cell__.items())) if isinstance(self.__cell__, dict) else self.__cell__,
|
def __hash__(self): |
|
return hash(( |
|
self.__forward_arg__, |
|
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__, |
|
self.__owner__, |
|
tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None, |
|
)) |
Code:
#!pip install beartype==0.22.9
from beartype import beartype
class MyClass:
@beartype
def foo(self) -> MyClass:
return MyClass()
print(MyClass().foo())
Observed behavior:
The program fails during decoration with the error: TypeError: unhashable type: 'cell'
File ".../beartype/_check/metadata/hint/hintsane.py", line 242, in __init__
self._hash = hash((hint, hint_recursable_to_depth, typearg_to_hint))
File ".../python3.14/annotationlib.py", line 291, in __hash__
return hash((..., tuple(sorted(self.__extra_names__.items()))))
TypeError: unhashable type: 'cell'
Inspection shows:
>>> self.__cell__
{'__classdict__': <cell at 0x...: dict object>}
>>> tuple(sorted(self.__cell__.items()))
(('__classdict__', <cell at 0x...>),)
Since cell objects are not hashable, this causes hash to fail.
Affected Versions:
Python 3.14.1 and 3.14.2
OS Tested on: MacOS
CPython versions tested on:
3.14
Operating systems tested on:
macOS
Linked PRs
Reacted by Bartosz Sławecki Activityjohnslavik commented on Jan 14, 2026 Smaller reproducer (without beartype):
import annotationlib
class A:
def m(self) -> A:
pass
A_fwdref = annotationlib.get_annotations(
m,
format=annotationlib.Format.FORWARDREF,
)['return']
hash(A_fwdref)johnslavik commented on Jan 14, 2026 Bisects to b1027d4 (GH-138164).
Details
git bisect start
# status: waiting for both good and bad commits
# bad: [d81e1ef0f3a7c63f5d246e4cf918700016b72489] gh-138189: Document type slots, and other constants, as part of Limited API (GH-138190)
git bisect bad d81e1ef0f3a7c63f5d246e4cf918700016b72489
# status: waiting for good commit(s), bad commit known
# good: [e34a5e33049ce845de646cf24a498766a2da3586] gh-140593: Fix a memory leak in function `my_ElementDeclHandler` of `pyexpat` (#140602)
git bisect good e34a5e33049ce845de646cf24a498766a2da3586
# good: [95a3564869daa3cc083e4d4603f5f03ee5f53e31] GH-140768: Warn when the WASI SDK version doesn't match the supported version (GH-140769)
git bisect good 95a3564869daa3cc083e4d4603f5f03ee5f53e31
# good: [121c219e302365e63eef0aef81b94a3de7a37fec] Remove redundant `sys.exit(2)` call in `pdb` CLI (#139948)
git bisect good 121c219e302365e63eef0aef81b94a3de7a37fec
# bad: [57f4d09a6fcfec229603f644c4254abdded00429] JIT: Fix compiler warning from visibility attribute in typedef (#139981)
git bisect bad 57f4d09a6fcfec229603f644c4254abdded00429
# bad: [478b8dab0b40f08c3ded40bf988ec49a50f4c8fb] Docs: Fix typo in codecs documentation (GH-140883)
git bisect bad 478b8dab0b40f08c3ded40bf988ec49a50f4c8fb
# good: [7a9437d98641e3c3749ab2fd9fb54eac7614f9af] gh-140348: Fix using | on unusual objects plus Unions (#140383)
git bisect good 7a9437d98641e3c3749ab2fd9fb54eac7614f9af
# bad: [b1027d4762435b97546c122dd94290d707b3ff39] gh-138151: Fix annotationlib handling of multiple nonlocals (#138164)
git bisect bad b1027d4762435b97546c122dd94290d707b3ff39
# good: [d590685297165d16df41003c3566b75a0a4ced61] gh-133879: Clean up What's New for 3.15 (#140435)
git bisect good d590685297165d16df41003c3566b75a0a4ced61
# first bad commit: [b1027d4762435b97546c122dd94290d707b3ff39] gh-138151: Fix annotationlib handling of multiple nonlocals (#138164)
johnslavik commented on Jan 14, 2026 @abhinand-c, the easiest workaround in your case with beartype is to stringify the annotation:
from beartype import beartype
class A:
@beartype
def m(self) -> 'A':
return 42
A().m() # raises BeartypeCallHintReturnViolation: Method __main__.A.m() return 42 violates type hint 'A', as int 42 not instance of <class "__main__.A">
EDIT: You found it too, nice :) beartype/beartype#610 (comment)
It makes deferred annotations blind to the class scope.
The unhashable cell in your reproduction is the transient class dict cell used in the annotation scope to resolve symbols in the class scope on the annotation level. The default __annotate__ functions of methods always consume __classdict__, so this regression will now reproduce everytime we try hash an annotationlib forward reference from any annotation in a method, because (1) this cell always appears (2) forward references are produced when eager resolution fails. johnslavik commented on Jan 14, 2026 Cells are not hashable. ForwardRef.__hash__ already provides an id for globals because dictionaries aren't hashable. Maybe we can do the same here?
|
def __hash__(self): |
|
return hash(( |
|
self.__forward_arg__, |
|
self.__forward_module__, |
|
id(self.__globals__), # dictionaries are not hashable, so hash by identity |
This problem didn't occur prior to the change because ForwardRef.__cell__ was always None. There was a comment saying ForwardRef.__cell__ et al. could become non-None if the stringifier object was manually assigned __class__:
|
# These are always set to None here but may be non-None if a ForwardRef |
|
# is created through __class__ assignment on a _Stringifier object. |
|
self.__globals__ = None |
|
self.__cell__ = None |
|
self.__extra_names__ = None |
However, this wasn't true for __cell__ at that time:
|
def transmogrify(self): |
|
for obj in self.stringifiers: |
|
obj.__class__ = ForwardRef |
|
obj.__stringifier_dict__ = None # not needed for ForwardRef |
|
if isinstance(obj.__ast_node__, str): |
|
obj.__arg__ = obj.__ast_node__ |
|
obj.__ast_node__ = None |
Only then b1027d4 delivered on that promise:
|
def transmogrify(self, cell_dict): |
|
for obj in self.stringifiers: |
|
obj.__class__ = ForwardRef |
|
obj.__stringifier_dict__ = None # not needed for ForwardRef |
|
if isinstance(obj.__ast_node__, str): |
|
obj.__arg__ = obj.__ast_node__ |
|
obj.__ast_node__ = None |
|
if cell_dict is not None and obj.__cell__ is None: |
|
obj.__cell__ = cell_dict |
Since then cells show up while hashing and we will run into this for every cell, not just in method annotations scenario. Reacted by Abhinand C and Savio Mak
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|