Skip to content

Commit 25d10b1

Browse files
committed
Make behaviour of __hash__ = None more intuitive: can be rolled back in subclasses, only allowed to override object.__hash__
1 parent 8c772c7 commit 25d10b1

File tree

4 files changed

+46
-5
lines changed

4 files changed

+46
-5
lines changed

mypy/checker.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2215,6 +2215,10 @@ def check_method_override_for_base_with_name(
22152215
# Will always fail to typecheck below, since we know the node is a method
22162216
original_type = NoneType()
22172217

2218+
if name == "__hash__" and isinstance(original_type, NoneType):
2219+
# Allow defining `__hash__` even if parent class was explicitly made unhashable
2220+
return False
2221+
22182222
always_allow_covariant = False
22192223
if is_settable_property(defn) and (
22202224
is_settable_property(original_node) or isinstance(original_node, Var)
@@ -3444,6 +3448,14 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, rvalue: Expression) ->
34443448
lvalue_node.name == "__slots__" and base.fullname == "builtins.object"
34453449
):
34463450
continue
3451+
if (
3452+
lvalue_node.name == "__hash__"
3453+
and base.fullname == "builtins.object"
3454+
and isinstance(get_proper_type(lvalue_type), NoneType)
3455+
):
3456+
# allow `__hash__ = None` if the overridden `__hash__` comes from object
3457+
# This isn't LSP-compliant, but too common in real code.
3458+
continue
34473459

34483460
if is_private(lvalue_node.name):
34493461
continue

mypy/semanal.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,6 +2030,15 @@ def analyze_class_body_common(self, defn: ClassDef) -> None:
20302030
self.setup_self_type()
20312031
defn.defs.accept(self)
20322032
self.apply_class_plugin_hooks(defn)
2033+
2034+
if "__eq__" in defn.info.names and "__hash__" not in defn.info.names:
2035+
# If a class defines `__eq__` without `__hash__`, it's no longer hashable.
2036+
hash_none = Var("__hash__", NoneType())
2037+
hash_none.info = defn.info
2038+
hash_none.set_line(defn)
2039+
hash_none.is_classvar = True
2040+
self.add_symbol("__hash__", hash_none, defn)
2041+
20332042
self.leave_class()
20342043

20352044
def analyze_typeddict_classdef(self, defn: ClassDef) -> bool:

test-data/unit/check-classes.test

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,7 @@ class Base:
116116
__hash__: None = None
117117

118118
class Derived(Base):
119-
def __hash__(self) -> int: # E: Signature of "__hash__" incompatible with supertype "Base" \
120-
# N: Superclass: \
121-
# N: None \
122-
# N: Subclass: \
123-
# N: def __hash__(self) -> int
119+
def __hash__(self) -> int:
124120
pass
125121

126122
# Correct:
@@ -147,6 +143,29 @@ class Base:
147143
class Derived(Base):
148144
__hash__ = 1 # E: Incompatible types in assignment (expression has type "int", base class "Base" defined the type as "Callable[[], int]")
149145

146+
[case testEqWithoutHash]
147+
class A:
148+
def __eq__(self, other) -> bool: ...
149+
150+
reveal_type(A.__hash__) # N: Revealed type is "None"
151+
[builtins fixtures/primitives.pyi]
152+
153+
[case testHashNoneOverride]
154+
class A:
155+
__hash__ = None
156+
157+
class B(A):
158+
def __hash__(self) -> int: return 0
159+
[builtins fixtures/primitives.pyi]
160+
161+
[case testHashNoneBadOverride]
162+
class A:
163+
def __hash__(self) -> int: return 0
164+
165+
class B(A):
166+
__hash__ = None # E: Incompatible types in assignment (expression has type "None", base class "A" defined the type as "Callable[[], int]")
167+
[builtins fixtures/primitives.pyi]
168+
150169
[case testOverridePartialAttributeWithMethod]
151170
# This was crashing: https://github.com/python/mypy/issues/11686.
152171
class Base:

test-data/unit/fixtures/primitives.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class object:
1010
def __str__(self) -> str: pass
1111
def __eq__(self, other: object) -> bool: pass
1212
def __ne__(self, other: object) -> bool: pass
13+
def __hash__(self) -> int: ...
1314

1415
class type:
1516
def __init__(self, x: object) -> None: pass

0 commit comments

Comments
 (0)