Repository: python/mypy

Issue:
Reachability check seems to consider isinstance(obj, Hashable) as always True
Running `mypy --warn-unreachable` (version 0.920) against the following snippet gives a false "unreachable statement" error.
```python
from collections.abc import Hashable
from typing import List, Tuple, Union

def wrap_hash(obj: Union[Tuple[int], List[int]]):
    if isinstance(obj, Hashable):
        return hash(obj)
    return hash(id(obj))  # error: Statement is unreachable  [unreachable]

wrap_hash((0,)) # reaches 1st return
wrap_hash([0])  # reaches 2nd return
```

We can even exchange the `Union` annotation for a single unhashable type like `List` or `Dict`, and still get the same unreachable error. Looks like `isinstance(obj, Hashable)` is just considered `True` for any type.

Is this a mypy bug? Can we add some hints into the example code so that mypy would figure out correct reachability?


Discussion:
The problem is that `__hash__` is defined on `object` as a real method.

```python
reveal_type(object.__hash__)  # N: Revealed type is "def (self: builtins.object) -> builtins.int"
```

We had this problem and discussion several times before. 
Maybe we can try to change it to `None | def (self: builtins.object) -> builtins.int`?
CC @JelleZijlstra 
We already have `__hash__: None` on list in typeshed. Why would `object` have to change?
I don't know how we can represent this idea in types, but: some `object` subtypes are hashable (`def (self: builtins.object) -> builtins.int`) and some are not (`None`). But, `object` defines that all subclasses are hashable by defining `__hash__` as `def (self: builtins.object) -> builtins.int`.

But, on the other hand, we know that `object` itself is hashable. Only its subtypes can be unhashable.
But mypy already correctly errors when a subclass of a class that fulfills a protocol doesn't fulfill it:
```python
from typing import Protocol

class X(Protocol):
    def foo(self, a: int) -> str:
        pass
    
class A:
    def foo(self, a: int) -> str:
        return ""
        
class B(A):
    def foo(self) -> str:  # type: ignore[override]
        return ""
        
a: X = A()
b: X = B()  # E: Incompatible types in assignment (expression has type "B", variable has type "X")
```
Why is `Hashable` different?
Looks like we have a bug in mypy 🤔 
For some reason `mypy` treats `Hashable` as plain `object`.

```
1 NameExpr(Hashable [typing.Hashable])
TypeInfo(
  Name(typing.Hashable)
  Bases(builtins.object)
  Mro(typing.Hashable, builtins.object)
  Names(
    __hash__)
  DeclaredMetaclass(abc.ABCMeta)
  MetaclassType(abc.ABCMeta))
2 def () -> builtins.object
```

I will look into it.

Resolving patch:
diff --git a/mypy/typeops.py b/mypy/typeops.py
--- a/mypy/typeops.py
+++ b/mypy/typeops.py
@@ -107,6 +107,10 @@ def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance,
     explicit_type = init_ret_type if is_new else orig_self_type
     if (
         isinstance(explicit_type, (Instance, TupleType))
+        # We have to skip protocols, because it can can be a subtype of a return type
+        # by accident. Like `Hashable` is a subtype of `object`. See #11799
+        and isinstance(default_ret_type, Instance)
+        and not default_ret_type.type.is_protocol
         # Only use the declared return type from __new__ or declared self in __init__
         # if it is actually returning a subtype of what we would return otherwise.
         and is_subtype(explicit_type, default_ret_type, ignore_type_params=True)


Test patch:
diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test
--- a/test-data/unit/pythoneval.test
+++ b/test-data/unit/pythoneval.test
@@ -1575,6 +1575,19 @@ reveal_type(x)  # Revealed type is "collections.OrderedDict[builtins.str, builti
 [out]
 _testTypingExtensionsOrderedDictAlias.py:3: note: Revealed type is "collections.OrderedDict[builtins.str, builtins.str]"
 
+[case testSpecialTypingProtocols]
+# flags: --warn-unreachable
+from typing import Awaitable, Hashable, Union, Tuple, List
+
+obj: Union[Tuple[int], List[int]]
+if isinstance(obj, Hashable):
+    reveal_type(obj)
+if isinstance(obj, Awaitable):
+    reveal_type(obj)
+[out]
+_testSpecialTypingProtocols.py:6: note: Revealed type is "Tuple[builtins.int]"
+_testSpecialTypingProtocols.py:8: error: Statement is unreachable
+
 [case testEnumValueWithPlaceholderNodeType]
 # https://github.com/python/mypy/issues/11971
 from enum import Enum