collections.abc.Hashable isinstance() Cache Goes Stale (Tested)
collections.abc.Hashable doesn't require subclassing -- its __subclasshook__ checks whether a class defines a usable __hash__, so ordinary classes pass isinstance(x, Hashable) automatically. That check runs through ABCMeta's C-level cache, which is keyed on the class and is not invalidated when the class is mutated afterward. Code that mutates a class's __hash__ at runtime -- a common pattern for making a mutable object temporarily unhashable, e.g. during an in-place edit -- can leave isinstance(x, Hashable) reporting the old, pre-mutation answer.
1. A plain class passes isinstance(..., Hashable) by default
from collections.abc import Hashable
class Foo:
pass
f = Foo()
print(isinstance(f, Hashable))
True
Measured: Foo never subclasses Hashable or registers with it. Hashable's __subclasshook__ checks whether the class provides a real __hash__ (every plain class inherits object.__hash__), and that's enough to pass the isinstance check.
2. Setting __hash__ = None makes the object genuinely unhashable
Foo.__hash__ = None
try:
hash(f)
except TypeError as e:
print("hash() ->", e)
hash() -> unhashable type: 'Foo'
Measured: after this assignment, calling hash(f) on the exact same object raises TypeError immediately -- Foo is now unhashable, matching how setting __hash__ = None is documented to work for mutable types.
3. isinstance() against Hashable never re-checks
print(isinstance(f, Hashable))
True
Measured: the same isinstance(f, Hashable) call that correctly returned True before the mutation still returns True afterward, on the same object, in the same process, with no other code running in between. A guard like if isinstance(x, Hashable): d[x] = 1 written against this object will pass the check and then raise TypeError on the very next line.
4. The cached result survives even through unrelated ABC activity
print(issubclass(Foo, Hashable)) # same stale answer via issubclass directly
d = {}
if isinstance(f, Hashable):
d[f] = 1 # raises despite the isinstance guard just above
True
Traceback (most recent call last):
...
TypeError: unhashable type: 'Foo'
Measured: issubclass(Foo, Hashable) shows the same stale True, since isinstance is implemented in terms of it. The dict insert immediately after the passing guard raises TypeError -- the isinstance check gave no real protection because nothing re-validates the cached answer once the class itself is mutated.
Frequently asked questions
Why does isinstance(x, Hashable) stay True after I set __hash__ = None?
We measured that ABCMeta caches the result of its subclass/instance checks keyed on the class. Hashable's check is satisfied once (the class has a usable __hash__), and that cached True is what later isinstance() calls return, even after the class's __hash__ is reassigned to None.
Is this a bug in collections.abc.Hashable?
It's documented ABC caching behavior, not specific to Hashable -- ABCMeta caches isinstance/issubclass results for performance and the cache isn't designed to track later mutation of the classes it already evaluated. Hashable just makes the consequence easy to demonstrate because __hash__ = None has an immediate, checkable effect on hash().
How do I reliably check if an object is hashable right before using it as a dict key?
We measured that calling hash(x) directly inside a try/except TypeError is accurate at that exact moment, unlike isinstance(x, Hashable), which can report a cached answer from before any later mutation of the object's class.
Does this only affect classes that change __hash__ at runtime?
Yes -- a class whose __hash__ is never reassigned after creation won't hit this, since there's no later mutation for the isinstance cache to miss. It specifically affects code that flips a class between hashable and unhashable (or vice versa) after other code has already run an isinstance/issubclass check against it.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in collections.abc.Hashable: created a plain class, confirmed isinstance(instance, Hashable) is True (it passes Hashable's default __subclasshook__ by having a non-None __hash__), then set the class's __hash__ to None afterward and re-ran the same isinstance() call on the same object. PlainHeaders is our extension; Python's collections.abc module is built in.