Search code examples
pythondjangodjango-2.2

Django model objects became not hashable after upgrading to django 2.2


I'm testing the update of an application from Django 2.1.7 to 2.2.12. I got an error when running my unit tests, which boils down to a model object not being hashable :

    Station.objects.all().delete()
py37\lib\site-packages\django\db\models\query.py:710: in delete
    collector.collect(del_query)
py37\lib\site-packages\django\db\models\deletion.py:192: in collect
    reverse_dependency=reverse_dependency)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <django.db.models.deletion.Collector object at 0x000001EC78243E80>
objs = <QuerySet [<Station(nom='DUNKERQUE')>, <Station(nom='STATION1')>, <Station(nom='STATION2')>]>, source = None, nullable = False
reverse_dependency = False

    def add(self, objs, source=None, nullable=False, reverse_dependency=False):
        """
        Add 'objs' to the collection of objects to be deleted.  If the call is
        the result of a cascade, 'source' should be the model that caused it,
        and 'nullable' should be set to True if the relation can be null.

        Return a list of all objects that were not already collected.
        """
        if not objs:
            return []
        new_objs = []
        model = objs[0].__class__
        instances = self.data.setdefault(model, set())
        for obj in objs:
>           if obj not in instances:
E           TypeError: unhashable type: 'Station'

Instances of model objects are hashable in Django, once they are saved to database and get a primary key.

I don't understand where the error comes from and why I get this when running this basic code:

In [7]: s = Station.objects.create(nom='SOME PLACE')

In [8]: hash(s)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-9333020f3184> in <module>
----> 1 hash(s)

TypeError: unhashable type: 'Station'

In [9]: s.pk
Out[9]: 2035

All this code works fine when I switch back to Django 2.1.7. The same happens with other model objects in the app. I'm using python version 3.7.2 on Windows, with a SQlite backend (on the development workstation).

Edit: Here's the definition of the model referred to above:

class Station(models.Model):
    nom = models.CharField(max_length=200, unique=True)

    def __str__(self):
        return self.nom

    def __repr__(self):
        return "<Station(nom='{}')>".format(self.nom)

    def __eq__(self, other):
        return isinstance(other, Station) and self.nom == other.nom

Solution

  • As pointed out by @Alasdair, the issue was a change of behaviour brought in Django 2.2 to comply with how a model class should behave when __eq__() is overriden but not __hash__(). As per the python docs for __hash__():

    A class that overrides __eq__() and does not define __hash__() will have its __hash__() implicitly set to None.

    More information about the implementation of this behaviour in Django can be found in this ticket.

    The fix can be either the one suggested in the ticket, i.e. re-assigning the __hash__() method of the model to the one of the super class: __hash__ = models.Model.__hash__

    Or a more object-oriented way could be:

        def __hash__(self):
            return super().__hash__()
    

    This seems a bit weird because this should be unnecessary: by default, a call to __hash__() should use the method from the super class where it's implemented. This suggests Django breaks encapsulation somehow. But maybe I don't understand everything. Anyway that's a sidenote.

    In my case, I still wanted to be able to compare model instances not yet saved to the database for testing purposes and ended up with this implementation :

        def __hash__(self):
            if self.pk is None:
                return hash(self.nom)
            return super().__hash__()