18

The following will work, but I'd rather not need to repeat the __hash__ in each subclass. Is there a way to tell the dataclass to inherit the hash function (i.e. not set it to None)?

from dataclasses import dataclass


@dataclass
class Hashable:

    def __hash__(self):
        hashed = hash((
            getattr(self, key)
            for key in self.__annotations__
            ))
        return hashed


@dataclass
class Node(Hashable):
    name: str = 'Undefined'

    def __hash__(self):
        return Hashable.__hash__(self)
Brian Bruggeman
  • 5,008
  • 2
  • 36
  • 55
  • 1
    Any particular reason you want to define that hash yourself? It's basically a buggier, less safe version of what you'd get if you set `frozen=True`. – user2357112 Dec 31 '18 at 18:26

1 Answers1

17

The reason your __hash__ is being set to None is that dataclasses is trying to stop you from shooting yourself in the foot. Your second class has eq=True for the dataclass decorator (this is the default value). From the docs:

Here are the rules governing implicit creation of a __hash__() method. Note that you cannot both have an explicit __hash__() method in your dataclass and set unsafe_hash=True; this will result in a TypeError.

If eq and frozen are both true, by default dataclass() will generate a __hash__() method for you. If eq is true and frozen is false, __hash__() will be set to None, marking it unhashable (which it is, since it is mutable). If eq is false, __hash__() will be left untouched meaning the __hash__() method of the superclass will be used (if the superclass is object, this means it will fall back to id-based hashing).

So just pass eq=False:

In [1]: from dataclasses import dataclass
   ...:
   ...:
   ...: @dataclass
   ...: class Hashable:
   ...:
   ...:     def __hash__(self):
   ...:         hashed = hash((
   ...:             getattr(self, key)
   ...:             for key in self.__annotations__
   ...:             ))
   ...:         return hashed
   ...:
   ...:
   ...: @dataclass(eq=False)
   ...: class Node(Hashable):
   ...:     name: str = 'Undefined'
   ...:

In [2]: hash(Node())
Out[2]: -9223372036579626267

However, as pointed out in the comments, this isn't very safe, since you have a mutable object that is now hash-able, and inconsistently so with it's implementation of __eq__, which it is inheriting from Hashable

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • 1
    I'd say the real problem is that they're defining this `__hash__` at all instead of using `frozen=True`. – user2357112 Dec 31 '18 at 18:41
  • @user2357112 I agree – juanpa.arrivillaga Dec 31 '18 at 18:41
  • 1
    @user2357112 This example is necessarily simple. Its a poor assumption to assume that `frozen` is even a good idea for my use case. – Brian Bruggeman Dec 31 '18 at 18:42
  • @BrianBruggeman: Well, your example `__hash__` is buggy (incorrectly handling `ClassVar`, `InitVar`, and inherited fields), and if you specifically don't want `frozen=True`, then it sounds like you want mutable hashable objects, which are usually a bad idea. Even if you don't want `frozen=True`, `unsafe_hash=True` is probably a better idea. – user2357112 Dec 31 '18 at 18:45
  • 1
    Unsafe really only handles simple primitives and won't work with more complexity. I'd like a reusable function I can both test and pass down a chain of subclasses. having dataclass(eq=False) is actually more obfuscating/less clear than my original example. – Brian Bruggeman Dec 31 '18 at 18:49