Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

__hash__ = None generates an error #4266

Open
bmerry opened this issue Nov 19, 2017 · 12 comments
Open

__hash__ = None generates an error #4266

bmerry opened this issue Nov 19, 2017 · 12 comments

Comments

@bmerry
Copy link
Contributor

bmerry commented Nov 19, 2017

In mypy 0.550 with --strict-optional, defining an unhashable class like so:

class Foo:
    __hash__ = None

gives

hash.py:2: error: Incompatible types in assignment (expression has type "None", base class "object" defined the type as "Callable[[object], int]")
@gvanrossum
Copy link
Member

Is this a regression? Have you tried it with earlier versions? Or are you just mentioning 0.550 to indicate you're up to date?

@bmerry
Copy link
Contributor Author

bmerry commented Nov 20, 2017

I hadn't tried earlier versions - just indicating that I'm up-to-date.

I've now tested with 0.521, 0.530 and 0.540 and get the same behaviour - so it's not a regression, or at least not a recent one.

@elazarg
Copy link
Contributor

elazarg commented Nov 20, 2017

Doesn't __hash__ = None # type: ignore do the trick?

@bmerry
Copy link
Contributor Author

bmerry commented Nov 20, 2017

Sure, that suppresses the error, but it seems like mypy (or possibly typeshed) has the wrong model of __hash__ if considers the type to be Callable[[object], int] rather than Optional[Callable[[object], int]] since hashability is optional.

Should I file this against typeshed instead?

@elazarg
Copy link
Contributor

elazarg commented Nov 20, 2017

No, this is not the wrong idea. Hashability is optional by convention, but as far as substitutability goes, once you define __hash__ once (and it is defined in object) subclasses must adhere to this protocol:

def f(x: object) -> Dict[object, int]:  # return type is valid
    return {x: 5}  # must be valid

This is especially true when strict-optional exists, where it would otherwise force everyone to check for hashability before using hash which is of course unacceptable. For a purist, the fact that __hash__ is defined in object is unfortunate, but this is how the language works.

The fact that hashability is optional seems to work pretty well with you declaring not to subtype object by using __hash__ = None # type: ignore. It's true that this is not always tracked - assigning to object will lose type information, in this case, and mypy will not warn about it. Do you have any other issues with it? Or any suggestions as to what mypy should do with it? The mere complaint issued by mypy is technically correct and the workaround seems simple to me.

@bmerry
Copy link
Contributor Author

bmerry commented Nov 20, 2017

If you're happy that mypy is working as designed then you can close this. However, it seems like typing.Hashable can't possibly work in strict-optional mode, even if it is re-implemented in terms of typing_extensions.Protocol. For example:

#!/usr/bin/env python3
from typing_extensions import Protocol

class Hashable(Protocol):
    def __hash__(object) -> int: ...

class Foo:
    __hash__ = None   # type: ignore

def print_hash(obj: Hashable):
    print(hash(obj))

print_hash(3)
print_hash(Foo())

fails to identify that print_hash(Foo()) is malformed. On the other hand, removing the type: ignore and running without --strict-optional, it actually works as intended (hashable.py:14: error: Argument 1 to "print_hash" has incompatible type "Foo"; expected "Hashable").

@elazarg
Copy link
Contributor

elazarg commented Nov 20, 2017

So the issue is about making mypy not forget that the class is unhashable in the presence of type: ignore (or similar comment). That might be nice feature to add.

It will not be bullet-proof unless we'll make Foo incompatible with object, which is theoretically plausible but probably completely impractical. So you will still have:

x: object = Foo()
print_hash(x)  # mypy cannot complain here

@ilevkivskyi
Copy link
Member

Just FYI on current master typing.Hashable is already a protocol (as well as several other similar things, typing.Iterable etc.)

@ilevkivskyi
Copy link
Member

This is needed to allow python/typeshed#2221

@NiklasRosenstein
Copy link

NiklasRosenstein commented Aug 14, 2020

A class that does not declare a base implicitly inherits from object, and object provides a __hash__ method. But should that mean hashability is not optional in any case in a statically typed environment?

class A:
  __hash__ = None
print(A.__bases__)  # (<class 'object'>,)
$ mypy --version
mypy 0.782
$ mypy test.py
test.py:3: error: Incompatible types in assignment (expression has type "None", base class "object" defined the type as "Callable[[object], int]")

@gvanrossum
Copy link
Member

(The key request here is formulated in @elazarg's comment above: #4266 (comment))

@oscarbenjamin
Copy link

It isn't necessary to set __hash__ = None if a class defines __eq__ but not __hash__. I don't know if mypy understands that but when I found this error I realised that I could simply delete the __hash__ = None line with no effect. On the other hand if you have a class A that defines __eq__ and __hash__ and then a subclass B with __hash__ = None then that is a violation of substitutability since instances of A and B are not interchangeable in any context that needs hashing. If you don't define __eq__ then I'm not sure what the point of setting __hash__ = None is.

You can see from this that __hash__ is set to None but mypy doesn't complain in this case:

class A:
    def __eq__(self, other):
        return False

print(A.__hash__)

@JukkaL JukkaL changed the title __hash__ = None fails in strict-optional mode __hash__ = None generates an error Mar 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants