collections.abc.Mapping __eq__ Requires Hashable Keys, Nothing Else Does (tested)

collections.abc.Mapping only requires you to implement __getitem__, __len__, and __iter__ -- nothing in that contract says keys must be hashable, and a mapping backed by a linear scan over (key, value) pairs (rather than a real dict) can legitimately support unhashable keys like lists or other mappings. Every mixin method built from those three -- keys(), values(), items(), get(), __contains__ -- works fine on such a mapping. The inherited __eq__ is the one exception: its implementation is dict(self.items()) == dict(other.items()), which builds a real dict internally and therefore demands hashable keys, even though hashability was never part of the abstract contract it's supposed to be satisfying.

1. A Mapping backed by a linear scan supports unhashable keys

from collections.abc import Mapping

class ListKeyMapping(Mapping):
    def __init__(self, pairs):
        self._pairs = list(pairs)
    def __getitem__(self, key):
        for k, v in self._pairs:
            if k == key:
                return v
        raise KeyError(key)
    def __iter__(self):
        return (k for k, v in self._pairs)
    def __len__(self):
        return len(self._pairs)

m = ListKeyMapping([(['a'], 1), (['b'], 2)])
print(list(m.keys()))
print(list(m.items()))
print(m.get(['a']))
[['a'], ['b']]
[(['a'], 1), (['b'], 2)]
1

Measured: the keys are plain Python lists, which are unhashable -- a real dict could never use them as keys. ListKeyMapping only implements the three required abstract methods and never hashes a key anywhere, so keys(), items(), and get() (all mixin methods built on top of those three) work correctly.

2. == crashes -- even comparing the mapping to itself

try:
    m == m
except TypeError as e:
    print("m == m ->", e)
m == m -> unhashable type: 'list'

Measured: m == m -- the exact same object, no second mapping involved -- raises TypeError. Comparing against a non-Mapping type like m == 5 still works and correctly returns False (the mixin checks isinstance(other, Mapping) first and returns NotImplemented if it fails); the crash is specific to comparing two actual Mappings.

3. Why: the mixin builds a real dict internally

import inspect
from collections.abc import Mapping
# Mapping.__eq__ is implemented (in CPython's _collections_abc.py) as:
#     def __eq__(self, other):
#         if not isinstance(other, Mapping):
#             return NotImplemented
#         return dict(self.items()) == dict(other.items())
# (shown above -- this is the actual mixin body, not reproduced output)

Measured: the inherited __eq__ doesn't compare key-by-key through the abstract interface; it calls the real dict() constructor on self.items() and other.items() and compares those two plain dicts. Building a dict requires hashing every key, so any key that isn't hashable blows up __eq__ specifically, regardless of how correctly __getitem__, __iter__, and __len__ are implemented.

4. The fix: override __eq__ yourself

class ListKeyMapping2(ListKeyMapping):
    def __eq__(self, other):
        if not isinstance(other, Mapping):
            return NotImplemented
        if len(self) != len(other):
            return False
        return all(other.get(k, object()) == v for k, v in self.items())
    __hash__ = None

m2 = ListKeyMapping2([(['a'], 1), (['b'], 2)])
print(m2 == m2)
True

Measured: overriding __eq__ to walk items() directly instead of routing through dict() fixes the comparison without needing hashable keys. This is the same class of problem as Set's _from_iterable hook: a mixin method assumes something (a single-arg constructor, or here, hashable keys) that the required abstract methods never actually guarantee, and the assumption only surfaces once that specific derived method is used.

Frequently asked questions

Why does Mapping.__eq__ require hashable keys when __getitem__/__iter__/__len__ don't?

We measured that the inherited __eq__ is implemented as dict(self.items()) == dict(other.items()) in CPython's _collections_abc.py -- it builds real dict objects to compare, and building a dict requires hashing every key, even though nothing in Mapping's abstract contract (__getitem__, __len__, __iter__) requires keys to be hashable.

Can a collections.abc.Mapping subclass legitimately have unhashable keys?

Yes -- if __getitem__ is implemented as a linear scan (comparing keys with == instead of hashing them), unhashable keys like lists or other mappings work fine for every mixin method except the inherited __eq__, which is the only one that internally constructs a real dict.

Does m == m fail even when comparing a mapping to itself?

Yes. We measured this directly -- the equality check doesn't short-circuit on identity, so dict(self.items()) still gets built and still raises TypeError on unhashable keys even when both sides of == are the exact same object.

How do I fix __eq__ for a Mapping subclass with unhashable keys?

Override __eq__ yourself to compare via items() or get() directly instead of relying on the inherited mixin -- we measured that walking both mappings' entries manually (checking length, then checking every key/value pair with get()) avoids ever calling dict() or hash() on a key.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in collections.abc.Mapping: subclassed it with __getitem__/__iter__/__len__ backed by a linear scan over (key, value) pairs so the keys never need to be hashed, then exercised keys()/values()/items()/get()/repr() (all fine) followed by == (TypeError, even against itself). PlainHeaders is our extension; Python's collections.abc module is built in.