From 288f50d847b5dfe3a35e10f0d064beb7c0d137cb Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Sun, 1 Feb 2026 21:34:26 -0800 Subject: [PATCH 1/4] Improve interaction between --local-partial-types and hashability --- mypy/checker.py | 2 +- test-data/unit/check-protocols.test | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b2a444721604b..98ee1fc7a09c1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7671,7 +7671,7 @@ def handle_partial_var_type( (Used by checkexpr and checkmember.) """ in_scope, is_local, partial_types = self.find_partial_types_in_all_scopes(node) - if typ.type is None and in_scope: + if typ.type is None and (in_scope or is_dunder(node.name)): # 'None' partial type. It has a well-defined type. In an lvalue context # we want to preserve the knowledge of it being a partial type. if not is_lvalue: diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 5dc10b9736c43..7d173bc67dbe6 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2939,7 +2939,25 @@ class Gleemer: [case testPartialTypeProtocolHashable] -# flags: --no-strict-optional +# flags: --no-strict-optional --no-local-partial-types +from typing import Protocol + +class Hashable(Protocol): + def __hash__(self) -> int: ... + +class ObjectHashable: + def __hash__(self) -> int: ... + +class DataArray(ObjectHashable): + __hash__ = None + + def f(self, x: Hashable) -> None: + reveal_type([self, x]) # N: Revealed type is "builtins.list[builtins.object]" +[builtins fixtures/tuple.pyi] + + +[case testPartialTypeProtocolHashableLocalPartialTypes] +# flags: --no-strict-optional --local-partial-types from typing import Protocol class Hashable(Protocol): From 420ac8c00edfb9fa5844396121d7d0c3f6b85dd1 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Sun, 1 Feb 2026 23:47:58 -0800 Subject: [PATCH 2/4] slightly better fix --- mypy/checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 98ee1fc7a09c1..abd91e3ce2c4d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4470,6 +4470,7 @@ def infer_variable_type( is_lvalue_final=name.is_final, is_lvalue_member=isinstance(lvalue, MemberExpr), ) + and not (self.scope.active_class() and is_dunder(name.name)) and not self.no_partial_types ): # We cannot use the type of the initialization expression for full type @@ -7671,7 +7672,7 @@ def handle_partial_var_type( (Used by checkexpr and checkmember.) """ in_scope, is_local, partial_types = self.find_partial_types_in_all_scopes(node) - if typ.type is None and (in_scope or is_dunder(node.name)): + if typ.type is None and in_scope: # 'None' partial type. It has a well-defined type. In an lvalue context # we want to preserve the knowledge of it being a partial type. if not is_lvalue: From 0ec76b55cece3528a440411375b8f187858782ae Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Mon, 2 Feb 2026 00:13:20 -0800 Subject: [PATCH 3/4] narrower --- mypy/checker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index abd91e3ce2c4d..cda5d5a803df4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4470,7 +4470,11 @@ def infer_variable_type( is_lvalue_final=name.is_final, is_lvalue_member=isinstance(lvalue, MemberExpr), ) - and not (self.scope.active_class() and is_dunder(name.name)) + and not ( + self.scope.active_class() + and is_dunder(name.name) + and isinstance(get_proper_type(init_type), NoneType) + ) and not self.no_partial_types ): # We cannot use the type of the initialization expression for full type From 45f151bd63b60bcd44cd0a6dbd5ec7b71f562fab Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 2 Feb 2026 11:38:58 -0800 Subject: [PATCH 4/4] comment --- mypy/checker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index cda5d5a803df4..317d5bfeadf23 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4471,6 +4471,9 @@ def infer_variable_type( is_lvalue_member=isinstance(lvalue, MemberExpr), ) and not ( + # Trust None assignments to dunder methods + # This is a bit ad-hoc, but it improves protocol + # (non-)assignability, for instance `__hash__ = None` self.scope.active_class() and is_dunder(name.name) and isinstance(get_proper_type(init_type), NoneType)