pythonqthashpyqtqstandarditem

QStandardItem missing __hash__ method


I found when converting some Python2/Qt4 code to Python3/Qt5, that apparently QStandardItem can no longer be used as a dict key in as it doesn't have __hash__ implemented, and therefore is not considered immutable anymore.

Those two snippets show the issue:

PyQt4:

>>> from PyQt4 import QtGui
>>> a = QtGui.QStandardItem()
>>> b = {}
>>> b[a] = "1"
>>> a.__hash__()
2100390

PyQt5:

>>> from PyQt5 import QtGui
>>> a = QtGui.QStandardItem()
>>> b = {}
>>> b[a] = "1"
TypeError: unhashable type: 'QStandardItem'
>>> a.__hash__()
TypeError: 'NoneType' object is not callable

Why was the change done? Should I not use QStandardItem as a dict key?

The obvious workaround would be to subclass QStandardItem and reimplement a trivial version of __hash__ (which I've done). But is there anything I'm missing?


Solution

  • In Qt, there are three requirements for hashability:

    1. the type must be an assignable data type
    2. the type must define an == operator
    3. a qHash function must be defined for the type

    So if PyQt wants to maintain consistency with Qt, it should only define __hash__ when the above conditions apply, and its implementation of it should simply delegate to whatever qHash function Qt provides.

    The behaviour when using Python 2 with PyQt4/5 should probably be considered a misfeature, because the results it gives are not consistent with Qt. This can be seen by looking at what happens with a type that is hashable (in Qt terms):

    Using Python 3:

    >>> a = QtCore.QUrl('foo.bar')
    >>> b = QtCore.QUrl('foo.bar')
    >>> a == b
    True
    >>> hash(a) == hash(b)
    True
    

    This is exactly what we want: objects that compare equal, should also hash equal. But now look at what happens when using the same version of PyQt with Python 2:

    >>> a = Qt.QUrl('foo.bar')
    >>> b = Qt.QUrl('foo.bar')
    >>> a == b
    True
    >>> hash(a) == hash(b)
    False
    

    It seems that something like the object's identity is used by the __hash__ implementation in Python 2, which obviously won't ever be consistent with Qt's hashing semantics.

    The QStandardItem class has never been hashable in Qt, so for the sake of consistency, PyQt now chooses not to provide a __hash__ method for it. And since instances of QStandardItem are, in fact, mutable, PyQt quite reasonably leaves it up to the user to decide when to define __hash__, and how to implement it. For compatibility with Python 2, this could probably just return id(self).