2

Using structural pattern matching, how do you write a case that matches instances of hashable types?

I've tried:

for obj in [], (), set(), frozenset(), 10, None, dict():
    match obj:
        case object(__hash__=_):
            print('Hashable type:  ', type(obj))
        case _:
            print('Unhashable type: ', type(obj))

However, this gets the wrong answer because every type defines __hash__ whether it is hashable or not:

Hashable type:   <class 'list'>
Hashable type:   <class 'tuple'>
Hashable type:   <class 'set'>
Hashable type:   <class 'frozenset'>
Hashable type:   <class 'int'>
Hashable type:   <class 'NoneType'>
Hashable type:   <class 'dict'>
Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485

2 Answers2

3

Solution

The Hashable abstract base class in collections.abc can recognize types that implement hashing using tests like isinstance(obj, Hashable) or issubclass(cls, Hashable).

According to PEP 622, for a class pattern, "whether a match succeeds or not is determined by the equivalent of an isinstance call."

So, you can use Hashable directly in a class pattern:

from collections.abc import Hashable

for obj in [], (), set(), frozenset(), 10, None, dict():
    match obj:
        case Hashable():
            print('Hashable type:  ', type(obj))
        case _:
            print('Unhashable type:', type(obj))

This produces the desired answer:

Unhashable type: <class 'list'>
Hashable type:   <class 'tuple'>
Unhashable type: <class 'set'>
Hashable type:   <class 'frozenset'>
Hashable type:   <class 'int'>
Hashable type:   <class 'NoneType'>
Unhashable type: <class 'dict'>

Calls to hash() may still fail or be useless

Hashable only deals with the type of the outermost object. It reports hashability in the sense of "the object's type implements hashing" which is what we usually mean when we say "tuples are hashable". That is also the same sense that is used by abstract base classes and by static typing.

Though Hashable detects whether a type implements _hash_, it can not know what hashing actually does, whether it will succeed, or whether it will give consistent results.

For example, hashing gives inconsistent results for float('NaN'). Tuples and frozensets are normally hashable but will fail to hash if their components values are unhashable. A class could define __hash__ to always raise an exception.

Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • 1
    This treats `([1, 2], [3, 4])` as hashable, though. The only really reliable way to check whether something is hashable is to try to hash it. – user2357112 Nov 05 '21 at 05:20
  • It also looks up `__hash__` on the object instead of on its type, so it thinks things like `list` are unhashable because `list.__hash__` is `None`, even though `type(list).__hash__` isn't `None`. – user2357112 Nov 05 '21 at 05:26
  • "What we're detecting here is whether a class aspires to be hashable or unhashable." - but the question was how to detect whether a *specific object* is hashable, not whether its type has a `__hash__` method. There aren't a lot of cases where "this object's type has a `__hash__` method" is a more appropriate check than "this object can be successfully hashed". – user2357112 Nov 05 '21 at 05:29
3

Raymond Hettinger's answer works in limited cases, but it fails on inputs like list (the type object itself), which is hashable even though list.__hash__ is None, and inputs like ([1, 2], [3, 4]), which is unhashable even though tuple.__hash__ is not None.

The most reliable way to detect whether an object is hashable is always going to be to try to hash it. If you want to do that in a match statement, the easiest way would be to write a guard:

def hashable(x):
    try:
        hash(x)
    except TypeError:
        return False
    else:
        return True

match x:
    case _ if hashable(x):
        ...
    ...

This just directly calls hash(x) and sees whether it works, instead of trying to perform a structural check.

If you need the hash and want to avoid double-computation, you can save the hash(x) result:

def try_hash(x):
    try:
        return hash(x)
    except TypeError:
        return None

match x:
    case _ if (x_hash := try_hash(x)) is not None:
        ...
    ...
user2357112
  • 260,549
  • 28
  • 431
  • 505