Skip to content

[Bug] CPython 3.14.2 + PEP 649 annotationlib.ForwardRef + PEP 484 self-reference = TypeError: unhashable type: 'cell' 😨  #610

Closed

Description

@abhinand-c

Using @beartype with deferred (forward) annotations on Python 3.14 raises a TypeError: unhashable type: 'cell' at decoration time.

This occurs even for a simple self-referential return annotation inside a class.

from beartype import beartype

class MyClass:

    @beartype
    def foo(self) -> MyClass:
        return MyClass()

if __name__ == "__main__":
    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'

Expected behavior:

According to the Beartype documentation, deferred / forward annotations are supported starting from Beartype 0.22.2 (see: https://beartype.readthedocs.io/en/latest/pep/).

Environment:

Python: 3.14.2
beartype: 0.22.9

Activity

  1. Glinte commented on Jan 13, 2026

    @Glinte
    Contributor

    Debugging information

    # annotationlib
    class ForwardRef:
        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,
            ))
    Image
    >>> self.__cell__
    {'__classdict__': <cell at 0x0000029F34233BB0: dict object at 0x0000029F355BEEC0>}
    >>> isinstance(self.__cell__, dict)
    True
    >>> self.__cell__.items()
    dict_items([('__classdict__', <cell at 0x0000029F34233BB0: dict object at 0x0000029F355BEEC0>)])
    >>> self.__cell__["__classdict__"]
    <cell at 0x0000029F34233BB0: dict object at 0x0000029F355BEEC0>
    >>> type(self.__cell__["__classdict__"])
    <class 'cell'>
    >>> tuple(sorted(self.__cell__.items()))
    (('__classdict__', <cell at 0x0000029F34233BB0: dict object at 0x0000029F355BEEC0>),)

    Seems more likely to be an annotationlib problem because they defined __hash__ and included something non-hashable

    Upstream blame: python/cpython#138164 / python/cpython#140949 (3.14 port)

    Cells are a C object that I've never heard of: https://docs.python.org/3/c-api/cell.html

  2. leycec commented on Jan 13, 2026

    @leycec
    Member

    ...heh. Cells are how CPython implements closures at the C layer. Whenever you see the word "cell" in an exception traceback, your survival instinct should be to immediately run. 😂

    @Glinte is correct, as always. This is almost certainly a CPython 3.14 issue. That said, there might be something @beartype can (and should) do to mitigate this issue. If the standard annotationlib API is failing @beartype for common use cases, @beartype should consider (A) detecting these cases and (B) circumventing them by using our own non-standard (but working) @beartype-specific forward reference proxies rather than standard (but non-working) annotationlib machinery.

    Much to chew on here. Let us chew. And thanks so much to @Glinte for investigating this!

  3. leycec commented on Jan 14, 2026

    @leycec
    Member

    ...lol. I cannot believe CPython devs failed to properly unit test the annotationlib.ForwardRef.__hash__() dunder method. Actually, wait. Hold my bear. I can believe it. It's all too easy to believe.

    Based on @Glinte's superb analysis, this is clearly the problematic upstream line:

                tuple(sorted(self.__cell__.items())) if isinstance(self.__cell__, dict) else self.__cell__,

    I wonder why they thought that would ever work as advertised. Obviously:

    1. Dictionary values are not guaranteed to be hashable.
    2. Ergo, sorted(self.__cell__.items()) is also not guaranteed to be hashable.
    3. Ergo, tuple(sorted(self.__cell__.items())) is likely to raise TypeError exceptions in common use cases, which it does here.

    I facepalm two times. And then I facepalm two more times. 🤦 🤦‍♂️ 🤦‍♀️ 🤦

    Upstream: Why You Do @beartype Dirty Like This?

    I was hoping upstream would have already resolved this, but... that doesn't appear to be the case. If we can cobble together something that works, @beartype could just monkey-patch this possibly broken annotationlib.ForwardRef.__hash__() implementation under older CPython versions.

    Also, did anyone notice that that implementation is stupidly slow? Because it is. Which completely defeats the point of a hash function. Hash functions are supposed to be fast. That's how you guarantee amortized O(1) dictionary lookup. But this thing? It uselessly recomputes a huge recursive hash across the entire universe every time you call it. And the worst part is...

    It doesn't even work. 🤦 🤦‍♂️ 🤦‍♀️ 🤦

    Not quite sure what to do, honestly. First things first...

    Upstream Needs to Know They Broke Stuff

    It would be wonderful if someone who is not me could submit this to the upstream CPython issue tracker. It might've been fixed already, but I suspect it hasn't. I am sad face.

    When someone who is not me reports this, please note that this worked under CPython 3.14.0. We could bisect exactly what went wrong if we cared, which we don't. But either CPython 3.14.1 or 3.14.2 introduced a regression that broke this use case.

    Next things next...

    I Can't Replicate This on My End

    Your example works without raising the expected exception on my local devbox, @abhinand-c:

    $ python3.14 run_abhinands_example.py
    <__main__.MyClass object at 0x7fd7d5d65a90>

    That looks sane. Admittedly, I'm using an older CPython 3.14 patch version:

    $ python3.14 --version
    Python 3.14.0

    Interestingly, the annotationlib.ForwardRef.__hash__() implementation is somewhat different under this older CPython 3.14 patch version:

        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__,
                self.__cell__,
                self.__owner__,
                tuple(sorted(self.__extra_names__.items())) if self.__extra_names__ else None,
            ))

    So, simply self.__cell__ rather than tuple(sorted(self.__cell__.items())) if isinstance(self.__cell__, dict) else self.__cell__,. I don't quite get it, though. Even if I replace the former with the latter, this still works on my local devbox. Clearly, there is another problematic annotationlib change somewhere that intersects with this issue in mysterious ways that exhaust me.

    Gods above! What has @beartype done to deserve this? I beseech thee.


    @beartype asks only this one question

  4. Glinte commented on Jan 14, 2026

    @Glinte
    Contributor

    I dont really want to make the upstream issue lol. and yes I reproduced on 3.14.2

  5. changed the title [-][Bug] TypeError: unhashable type: 'cell' when using deferred annotations with @beartype on Python 3.14[/-] [+][Bug] CPython 3.14.2 + PEP 649 `annotationlib.ForwardRef` + PEP 484 self-reference = `TypeError: unhashable type: 'cell'` 😨 [/+] on Jan 14, 2026
  6. leycec commented on Jan 14, 2026

    @leycec
    Member

    Right there with you. 🤣

    Let's do nothing and pray things look better in the morning. I believe in the power of the couch potato.

  7. abhinand-c commented on Jan 14, 2026

    @abhinand-c
    Author

    @leycec Yes, it seems that the issue is not there in Python 3.14.0, but introduced in 3.14.1 and is still present in 3.14.2. Tested the versions locally in MacOS.

  8. abhinand-c commented on Jan 14, 2026

    @abhinand-c
    Author

    @leycec I'll go ahead and raise the issue in CPython.

  9. leycec commented on Jan 14, 2026

    @leycec
    Member

    Wonderful. Thanks so much for going the extra mile. Until some combination of us and upstream resolves this, would you mind lightly refactoring your code? In theory, your example should work if you just move the @beartype decorator up from the method to its parent class: e.g.,

    from beartype import beartype
    
    @beartype  # <-- should work, but no idea. let's wring our hands as our forehead sweats.
    class MyClass:
        def foo(self) -> MyClass:
            return MyClass()
    
    if __name__ == "__main__":
        print(MyClass().foo())

    Does that work? If not, we're really in trouble. 😅

  10. abhinand-c commented on Jan 14, 2026

    @abhinand-c
    Author

    For anyone else running into this issue, I worked around it by reverting to the older string-based type annotation for the self-referencing return type. Using "MyClass" instead of the direct class reference avoids the error for now:

    from beartype import beartype
    
    
    class MyClass:
    
        @beartype
        def foo(self) -> "MyClass":
            return MyClass()
    
    if __name__ == "__main__":
        print(MyClass().foo())
    
  11. abhinand-c commented on Jan 14, 2026

    @abhinand-c
    Author

    Wonderful. Thanks so much for going the extra mile. Until some combination of us and upstream resolves this, would you mind lightly refactoring your code? In theory, your example should work if you just move the @beartype decorator up from the method to its parent class: e.g.,

    from beartype import beartype

    @beartype # <-- should work, but no idea. let's wring our hands as our forehead sweats.
    class MyClass:
    def foo(self) -> MyClass:
    return MyClass()

    if name == "main":
    print(MyClass().foo())
    Does that work? If not, we're really in trouble. 😅

    This one still throws the same error: TypeError: unhashable type: 'cell'

  12. leycec commented on Jan 14, 2026

    @leycec
    Member

    Indeed! Excellent workaround. If anyone's curious, the reason an old-school stringified forward reference works is that @beartype then has to use its own @beartype-specific forward reference proxy rather than reusing Python's generic forward reference proxy (i.e., annotationlib.ForwardRef object). Unlike Python, we test. That said:

    This one still throws the same error: TypeError: unhashable type: 'cell'

    OHNOES. That means self-references are super-broken under the newest CPython release. But that was the whole point of PEPs 649 and 749 and Python 3.14 itself! Our entire summer was wasted. Oh, well. That's life. Sometimes you waste an entire summer. It's unclear how exactly we can fix this on our end, because it's unclear how exactly CPython even broke this. For the moment:

    • Downstream users should consider blacklisting CPython 3.14.1 and 3.14.2 in their pyproject.toml.

    Not a great solution, obviously. Actually, that's not a solution at all. 😭

  13. abhinand-c commented on Jan 14, 2026

    @abhinand-c
    Author

    @leycec @Glinte I have created the issue, please do review and comment: python/cpython#143831

  14. abhinand-c commented on Jan 15, 2026

    @abhinand-c
    Author

    python/cpython#143848 has been created by @johnslavik to address the issue. It has fixed the issue.

    Also, any thoughts on how to improve the performance of the implementation.

  15. Glinte commented on Jan 15, 2026

    @Glinte
    Contributor

    I have no comment, lets hope leycec has something to say

  16. leycec commented on Jan 15, 2026

    @leycec
    Member

    Lo, I have said things! But are they useful things? These tears of exhaustion say: "They'd better be." 🥹

  17. leycec commented on Feb 8, 2026

    @leycec
    Member

    Bwa-hah! Let's pretend I did something to resolve this for everyone. Instead, I did nothing. Pretty sure this issue is on CPython's end, but please do reopen if there's something @beartype can and/or should be doing for the QA masses here. 😅

    Hope everyone's having a fantastic winter, by the winter. It's so bitterly cold here in the frozen wastes of the Canadian wilderness that... I can see my own breath while typing this. I am shivering. The cat is shivering. That's just how Canada rolls. Like a snowman with a puckered face.


    puckered-face snowman says: "sup, mah qa homies."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions