64

I am interested in taking an arbitrary dict and copying it into a new dict, mutating it along the way.

One mutation I would like to do is swap keys and value. Unfortunately, some values are dicts in their own right. However, this generates a "unhashable type: 'dict'" error. I don't really mind just stringifying the value and giving it the key. But, I'd like to be able to do something like this:

for key in olddict:
  if hashable(olddict[key]):
    newdict[olddict[key]] = key
  else
    newdict[str(olddict[key])] = key

Is there a clean way to do this that doesn't involve trapping an exception and parsing the message string for "unhashable type" ?

LightCC
  • 9,804
  • 5
  • 52
  • 92
Paul Nathan
  • 39,638
  • 28
  • 112
  • 212

4 Answers4

75

Python 3.x

Use collections.abc.Hashable or typing.Hashable.

>>> import typing
>>> isinstance({}, typing.Hashable)
False
>>> isinstance(0, typing.Hashable)
True

Note: both are the same one, the latter is simply an alias of the former. Also note that collections.Hashable was removed in Python 3.10+ (deprecated since 3.7).

Python 2.6+ (an original answer)

Since Python 2.6 you can use the abstract base class collections.Hashable:

>>> import collections
>>> isinstance({}, collections.Hashable)
False
>>> isinstance(0, collections.Hashable)
True

This approach is also mentioned briefly in the documentation for __hash__.

Doing so means that not only will instances of the class raise an appropriate TypeError when a program attempts to retrieve their hash value, but they will also be correctly identified as unhashable when checking isinstance(obj, collections.Hashable) (unlike classes which define their own __hash__() to explicitly raise TypeError).

Jongwook Choi
  • 8,171
  • 3
  • 25
  • 22
Mark Byers
  • 811,555
  • 193
  • 1,581
  • 1,452
  • 11
    No, and this is something you can determine only at run-time since before that the content of a list is generally unknown. `hash(([],))` gives `TypeError: unhashable type: 'list'` – Joshua Chia May 07 '14 at 10:28
  • 1
    Small warning for people using Python 2.7 `isinstance(bytearray([0xa]), collections.Hashable))` returns `True` but `hash(bytearray([0xa]))` fails with `TypeError: unhashable type: 'bytearray'`. – RedX Oct 07 '16 at 10:41
  • 1
    Anyone have any explanations for this one? `isinstance(Decimal('sNaN'),Hashable)` returns `True` yet when its given to a dictionary, I get `Cannot hash a signaling NaN value` – codykochmann Jul 21 '17 at 19:55
  • 8
    In Python 3 it's `isinstance(obj, collections.abc.Hashable)`. – martineau Nov 05 '18 at 07:07
  • 9
    In Python 3.7.5, importing directly from `collections` raises `DeprecationWarning`. Instead, `from collections.abc import Hashable`. – David Lord Dec 10 '19 at 08:14
  • 5
    **This is awful.** `isinstance(obj, collections.abc.Hashable)` returns false positives for user-defined classes that raise exceptions from overridden `__hash__()` dunder methods. `collections.abc.Hashable` *cannot* be trusted for real-world hashable detection. Instead, the only sane solution is to attempt to `hash(obj)` and report `False` if doing so raises *any* exceptions whatsoever. – Cecil Curry Jul 02 '20 at 06:31
  • 1
    Can the author please update this answer? – Rylan Schaeffer May 30 '22 at 00:11
  • @martineau, want to post that as an answer? – Daniel Walker Nov 09 '22 at 14:57
  • I have updated this accepted answer with how we are supposed to do on Python 3. – Jongwook Choi Nov 30 '22 at 16:12
  • I, too, can confirm that the answer is misleading. In the `uncertainties` package, a numpy array of uncertain values, call in `arr`, leads to isinstance(arr[0], typing.Hashable)==True, but hash(arr[0]) raising an exception that AffineScalarFunc type is not hashable. – Michael Tiemann Aug 05 '23 at 16:13
25
def hashable(v):
    """Determine whether `v` can be hashed."""
    try:
        hash(v)
    except TypeError:
        return False
    return True
Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • 5
    Ned - that's exactly what I would prefer to avoid. Also, this function will trap TypeErrors that aren't "unhashable type". – Paul Nathan Aug 11 '10 at 16:46
  • 11
    There's only one TypeError that hash() will raise. Or alternately, whatever TypeError hash raises, it's going to keep your value from being hashed. I'd even argue that this should catch Exception, because it doesn't really matter why hash() failed: a failure in hash() makes your value unhashable. Can you say more about why you want to avoid exceptions like this? This encapsulates the exception handling in a nice function, and makes your sample code above work perfectly. – Ned Batchelder Aug 11 '10 at 16:49
  • 2
    Perhaps it's more of a philosophical point - the question isn't about the "exceptional condition" of being unhashable due to some sort of "freak condition", but a query about the functionality available on a type. If that makes any sense. – Paul Nathan Aug 11 '10 at 16:53
  • 6
    I think you're right about it being a philosophical point, and perhaps comes down to a static vs. dynamic view of the world. The classic Python mindset often tries things and catches exceptions, rather than attempting to determine upfront if something in theory is possible. – Ned Batchelder Aug 11 '10 at 16:58
  • 4
    @Paul Nathan: This is the **standard** approach. Simply attempt the operation; if it fails, then the object was not hashable; do something else. If it works, then the object was hashable and you did what you expected to do. – S.Lott Aug 11 '10 at 16:59
  • 2
    This method is not good practice! Using exception for checking such things is discouraged because of stack walking and so – Kamyar Jun 27 '20 at 08:11
  • 4
    **This is awful.** The `hash()` builtin implicitly calls the user-defined `v.__hash__()` dunder method, which could conceivably raise *any* possible exception – not simply `TypeError` exceptions. To generalize this, replace the overly specific subclass `TypeError` above with the catch-all exception superclass `Exception`. – Cecil Curry Jul 02 '20 at 06:29
  • For my project, I am trying to implement a recursive approach that hashes the hashables, and then unpacks lists and class instance and hashes the pieces recursively. I will implement the test as suggested, but catching any Exception. – Steve L Apr 20 '23 at 15:52
4

All hashable built in python objects have a .__hash__() method. You can check for that.

olddict = {"a":1, "b":{"test":"dict"}, "c":"string", "d":["list"] }

for key in olddict:
   if(olddict[key].__hash__):
      print str(olddict[key]) + " is hashable"
   else: 
      print str(olddict[key]) + " is NOT hashable"

output

1 is hashable
string is hashable
{'test': 'dict'} is NOT hashable
['list'] is NOT hashable
Chandler
  • 1,019
  • 14
  • 23
  • 3
    A warning about this: In Python 2.5 this will give: `{'test': 'dict'}is hashable`. It can also give a wrong result in newer versions if a class defines `__hash__` to raise a TypeError. – Mark Byers Aug 11 '10 at 17:31
  • 4
    **This is awful,** too. Simply because a type defines a `__hash__()` method does *not* mean that all possible instances of that type are hashable. Consider `def __hash__(self): raise ValueError('uhoh')`, for example. Instead, the only sane solution is to attempt to `hash(olddict[key])` and report `False` if doing so raises any exceptions whatsoever. – Cecil Curry Jul 02 '20 at 06:34
2

Why not use duck typing?

for key in olddict:
   try:
       newdict[olddict[key]] = key
   except TypeError:
       newdict[str(olddict[key])] = key
dansalmo
  • 11,506
  • 5
  • 58
  • 53