Skip to content

Python 3.14.1+ annotationlib regression: ForwardRef.__hash__ raises TypeError: unhashable type: 'cell' #143831

Description

@abhinand-c

Bug 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

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Jan 14, 2026
  2. johnslavik commented on Jan 14, 2026

    @johnslavik
    Member

    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)
  3. johnslavik commented on Jan 14, 2026

    @johnslavik
    Member

    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)
    
  4. johnslavik commented on Jan 14, 2026

    @johnslavik
    Member

    @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.

  5. johnslavik commented on Jan 14, 2026

    @johnslavik
    Member

    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.

  6. self-assigned this
    on Jan 14, 2026
  7. added a commit that references this issue on Jan 19, 2026
  8. added a commit that references this issue on Jan 19, 2026
  9. added a commit that references this issue on Jan 19, 2026
  10. added a commit that references this issue on Feb 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

3.14bugs and security fixes3.15bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or error

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions