From a9120534eeeb43a1efae4a59eb4b7743ac4fe14f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 28 Feb 2026 18:28:01 +0000 Subject: [PATCH 1/2] Extend tests for dataclass hashability --- .../results/mypy/dataclasses_hash.toml | 19 +++++-- .../results/pyrefly/dataclasses_hash.toml | 11 +++- .../results/pyright/dataclasses_hash.toml | 11 +++- conformance/results/results.html | 4 +- .../results/zuban/dataclasses_hash.toml | 21 ++++++-- conformance/tests/dataclasses_hash.py | 51 ++++++++++++++++--- .../tests/generics_typevartuple_callable.py | 2 +- 7 files changed, 98 insertions(+), 21 deletions(-) diff --git a/conformance/results/mypy/dataclasses_hash.toml b/conformance/results/mypy/dataclasses_hash.toml index 7af6dcbdc..78a60b6a1 100644 --- a/conformance/results/mypy/dataclasses_hash.toml +++ b/conformance/results/mypy/dataclasses_hash.toml @@ -1,11 +1,24 @@ -conformant = "Partial" +conformant = "Unsupported" notes = """ +Does not synthesize `__hash__ = None` as a class attribute for unhashable dataclasses. +Does not report when an unhashable dataclass has `__hash__` called directly on an instance. Does not report when dataclass is not compatible with Hashable protocol. """ output = """ +dataclasses_hash.py:14: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] +dataclasses_hash.py:29: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] +dataclasses_hash.py:40: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] +dataclasses_hash.py:55: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] +dataclasses_hash.py:69: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] +dataclasses_hash.py:85: error: Expression is of type "Callable[[DC6], int]", not "None" [assert-type] +dataclasses_hash.py:102: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] """ conformance_automated = "Fail" errors_diff = """ -Line 15: Expected 1 errors -Line 32: Expected 1 errors +Line 17: Expected 1 errors +Line 18: Expected 1 errors +Line 43: Expected 1 errors +Line 44: Expected 1 errors +Line 14: Unexpected errors ['dataclasses_hash.py:14: error: Expression is of type "Callable[[object], int]", not "None" [assert-type]'] +Line 40: Unexpected errors ['dataclasses_hash.py:40: error: Expression is of type "Callable[[object], int]", not "None" [assert-type]'] """ diff --git a/conformance/results/pyrefly/dataclasses_hash.toml b/conformance/results/pyrefly/dataclasses_hash.toml index 2d674772b..4023cb369 100644 --- a/conformance/results/pyrefly/dataclasses_hash.toml +++ b/conformance/results/pyrefly/dataclasses_hash.toml @@ -3,6 +3,13 @@ conformance_automated = "Pass" errors_diff = """ """ output = """ -ERROR dataclasses_hash.py:15:16-22: `DC1` is not assignable to `Hashable` [bad-assignment] -ERROR dataclasses_hash.py:32:16-22: `DC3` is not assignable to `Hashable` [bad-assignment] +ERROR dataclasses_hash.py:17:1-16: Expected a callable, got `None` [not-callable] +ERROR dataclasses_hash.py:18:16-22: `DC1` is not assignable to `Hashable` [bad-assignment] +ERROR dataclasses_hash.py:29:12-32: assert_type((self: DC2) -> int, None) failed [assert-type] +ERROR dataclasses_hash.py:43:1-16: Expected a callable, got `None` [not-callable] +ERROR dataclasses_hash.py:44:16-22: `DC3` is not assignable to `Hashable` [bad-assignment] +ERROR dataclasses_hash.py:55:12-32: assert_type((self: DC4) -> int, None) failed [assert-type] +ERROR dataclasses_hash.py:69:12-32: assert_type((self: DC5) -> int, None) failed [assert-type] +ERROR dataclasses_hash.py:85:12-32: assert_type((self: DC6) -> int, None) failed [assert-type] +ERROR dataclasses_hash.py:102:12-32: assert_type((self: DC7) -> int, None) failed [assert-type] """ diff --git a/conformance/results/pyright/dataclasses_hash.toml b/conformance/results/pyright/dataclasses_hash.toml index bd6c15f84..3bc21bf98 100644 --- a/conformance/results/pyright/dataclasses_hash.toml +++ b/conformance/results/pyright/dataclasses_hash.toml @@ -1,13 +1,20 @@ conformant = "Pass" output = """ -dataclasses_hash.py:15:16 - error: Type "DC1" is not assignable to declared type "Hashable" +dataclasses_hash.py:17:1 - error: Object of type "None" cannot be called (reportOptionalCall) +dataclasses_hash.py:18:16 - error: Type "DC1" is not assignable to declared type "Hashable"   "DC1" is incompatible with protocol "Hashable"     "__hash__" is an incompatible type       Type "None" is not assignable to type "() -> int" (reportAssignmentType) -dataclasses_hash.py:32:16 - error: Type "DC3" is not assignable to declared type "Hashable" +dataclasses_hash.py:29:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC2) -> int" (reportAssertTypeFailure) +dataclasses_hash.py:43:1 - error: Object of type "None" cannot be called (reportOptionalCall) +dataclasses_hash.py:44:16 - error: Type "DC3" is not assignable to declared type "Hashable"   "DC3" is incompatible with protocol "Hashable"     "__hash__" is an incompatible type       Type "None" is not assignable to type "() -> int" (reportAssignmentType) +dataclasses_hash.py:55:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC4) -> int" (reportAssertTypeFailure) +dataclasses_hash.py:69:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC5) -> int" (reportAssertTypeFailure) +dataclasses_hash.py:85:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC6) -> int" (reportAssertTypeFailure) +dataclasses_hash.py:102:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC7) -> int" (reportAssertTypeFailure) """ conformance_automated = "Pass" errors_diff = """ diff --git a/conformance/results/results.html b/conformance/results/results.html index a99606a66..058d7e553 100644 --- a/conformance/results/results.html +++ b/conformance/results/results.html @@ -737,9 +737,9 @@

Python Type System Conformance Test Results

Pass      dataclasses_hash -
Partial

Does not report when dataclass is not compatible with Hashable protocol.

-Pass +
Unsupported

Does not synthesize `__hash__ = None` as a class attribute for unhashable dataclasses.

Does not report when an unhashable dataclass has `__hash__` called directly on an instance.

Does not report when dataclass is not compatible with Hashable protocol.

Pass +
Partial

Does not synthesize a `__hash__ = None` class attribute for unhashable dataclasses.

Pass      dataclasses_inheritance diff --git a/conformance/results/zuban/dataclasses_hash.toml b/conformance/results/zuban/dataclasses_hash.toml index 210b7f0bd..16ea55f2c 100644 --- a/conformance/results/zuban/dataclasses_hash.toml +++ b/conformance/results/zuban/dataclasses_hash.toml @@ -1,7 +1,22 @@ -conformance_automated = "Pass" +conformance_automated = "Fail" +conformant = "Partial" +notes = """ +Does not synthesize a `__hash__ = None` class attribute for unhashable dataclasses. +""" errors_diff = """ +Line 14: Unexpected errors ['dataclasses_hash.py:14: error: Expression is of type "Callable[[object], int]", not "None" [misc]'] +Line 40: Unexpected errors ['dataclasses_hash.py:40: error: Expression is of type "Callable[[object], int]", not "None" [misc]'] """ output = """ -dataclasses_hash.py:15: error: Incompatible types in assignment (expression has type "DC1", variable has type "Hashable") [assignment] -dataclasses_hash.py:32: error: Incompatible types in assignment (expression has type "DC3", variable has type "Hashable") [assignment] +dataclasses_hash.py:14: error: Expression is of type "Callable[[object], int]", not "None" [misc] +dataclasses_hash.py:17: error: "DC1" has no attribute "__hash__" [attr-defined] +dataclasses_hash.py:18: error: Incompatible types in assignment (expression has type "DC1", variable has type "Hashable") [assignment] +dataclasses_hash.py:29: error: Expression is of type "Callable[[object], int]", not "None" [misc] +dataclasses_hash.py:40: error: Expression is of type "Callable[[object], int]", not "None" [misc] +dataclasses_hash.py:43: error: "DC3" has no attribute "__hash__" [attr-defined] +dataclasses_hash.py:44: error: Incompatible types in assignment (expression has type "DC3", variable has type "Hashable") [assignment] +dataclasses_hash.py:55: error: Expression is of type "Callable[[object], int]", not "None" [misc] +dataclasses_hash.py:69: error: Expression is of type "Callable[[object], int]", not "None" [misc] +dataclasses_hash.py:85: error: Expression is of type "Callable[[DC6], int]", not "None" [misc] +dataclasses_hash.py:102: error: Expression is of type "Callable[[object], int]", not "None" [misc] """ diff --git a/conformance/tests/dataclasses_hash.py b/conformance/tests/dataclasses_hash.py index 7a5e68842..7bb68eb43 100644 --- a/conformance/tests/dataclasses_hash.py +++ b/conformance/tests/dataclasses_hash.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass -from typing import Hashable +from typing import Hashable, assert_type @dataclass @@ -11,7 +11,10 @@ class DC1: a: int -# This should generate an error because DC1 isn't hashable. +assert_type(DC1.__hash__, None) + +# These should generate errors because DC1 isn't hashable. +DC1(0).__hash__() # E v1: Hashable = DC1(0) # E @@ -20,7 +23,13 @@ class DC2: a: int -v2: Hashable = DC2(0) +# Because `DC2` is frozen, type checkers should synthesize +# a callable `__hash__` method for it, and therefore should +# emit a diagnostic here: +assert_type(DC2.__hash__, None) # E + +DC2(0).__hash__() # OK +v2: Hashable = DC2(0) # OK @dataclass(eq=True) @@ -28,7 +37,10 @@ class DC3: a: int -# This should generate an error because DC3 isn't hashable. +assert_type(DC3.__hash__, None) + +# These should generate errors because DC3 isn't hashable. +DC3(0).__hash__() # E v3: Hashable = DC3(0) # E @@ -37,7 +49,13 @@ class DC4: a: int -v4: Hashable = DC4(0) +# Because `DC4` is frozen, type checkers should synthesize +# a callable `__hash__` method for it, and therefore should +# emit a diagnostic here: +assert_type(DC4.__hash__, None) # E + +DC4(0).__hash__() # OK +v4: Hashable = DC4(0) # OK @dataclass(eq=True, unsafe_hash=True) @@ -45,7 +63,13 @@ class DC5: a: int -v5: Hashable = DC5(0) +# Type checkers should synthesize a callable `__hash__` +# method for `DC5` due to `unsafe_hash=True`, and therefore +# should emit a diagnostic here: +assert_type(DC5.__hash__, None) # E + +DC5(0).__hash__() # OK +v5: Hashable = DC5(0) # OK @dataclass(eq=True) @@ -56,7 +80,12 @@ def __hash__(self) -> int: return 0 -v6: Hashable = DC6(0) +# Type checkers should respect the manually defined `__hash__` +# method for `DC6`, and therefore should emit a diagnostic here: +assert_type(DC6.__hash__, None) # E + +DC6(0).__hash__() # OK +v6: Hashable = DC6(0) # OK @dataclass(frozen=True) @@ -67,4 +96,10 @@ def __eq__(self, other) -> bool: return self.a == other.a -v7: Hashable = DC7(0) +# Because `DC7` is frozen, type checkers should synthesize +# a callable `__hash__` method for it, and therefore should +# emit a diagnostic here: +assert_type(DC7.__hash__, None) # E + +DC7(0).__hash__() # OK +v7: Hashable = DC7(0) # OK diff --git a/conformance/tests/generics_typevartuple_callable.py b/conformance/tests/generics_typevartuple_callable.py index 11e596573..bc0ae168c 100644 --- a/conformance/tests/generics_typevartuple_callable.py +++ b/conformance/tests/generics_typevartuple_callable.py @@ -47,4 +47,4 @@ def func3(*args: * tuple[int, *Ts, T]) -> tuple[T, *Ts]: def has_int_and_str(a: int, b: str, c: float, d: complex): - assert_type(func3(a, b, c, d), tuple[float, str, complex]) + assert_type(func3(a, b, c, d), tuple[complex, str, float]) From 4c5db27d65b1bca098e61eb705892c4e54f3dd55 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 28 Feb 2026 21:48:27 +0000 Subject: [PATCH 2/2] Jelle's suggestion --- .../results/mypy/dataclasses_hash.toml | 13 +++----- .../results/pyrefly/dataclasses_hash.toml | 9 ++---- .../results/pyright/dataclasses_hash.toml | 9 ++---- .../results/zuban/dataclasses_hash.toml | 13 +++----- conformance/tests/dataclasses_hash.py | 31 ++++--------------- 5 files changed, 18 insertions(+), 57 deletions(-) diff --git a/conformance/results/mypy/dataclasses_hash.toml b/conformance/results/mypy/dataclasses_hash.toml index 78a60b6a1..08809153e 100644 --- a/conformance/results/mypy/dataclasses_hash.toml +++ b/conformance/results/mypy/dataclasses_hash.toml @@ -6,19 +6,14 @@ Does not report when dataclass is not compatible with Hashable protocol. """ output = """ dataclasses_hash.py:14: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] -dataclasses_hash.py:29: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] -dataclasses_hash.py:40: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] -dataclasses_hash.py:55: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] -dataclasses_hash.py:69: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] -dataclasses_hash.py:85: error: Expression is of type "Callable[[DC6], int]", not "None" [assert-type] -dataclasses_hash.py:102: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] +dataclasses_hash.py:36: error: Expression is of type "Callable[[object], int]", not "None" [assert-type] """ conformance_automated = "Fail" errors_diff = """ Line 17: Expected 1 errors Line 18: Expected 1 errors -Line 43: Expected 1 errors -Line 44: Expected 1 errors +Line 39: Expected 1 errors +Line 40: Expected 1 errors Line 14: Unexpected errors ['dataclasses_hash.py:14: error: Expression is of type "Callable[[object], int]", not "None" [assert-type]'] -Line 40: Unexpected errors ['dataclasses_hash.py:40: error: Expression is of type "Callable[[object], int]", not "None" [assert-type]'] +Line 36: Unexpected errors ['dataclasses_hash.py:36: error: Expression is of type "Callable[[object], int]", not "None" [assert-type]'] """ diff --git a/conformance/results/pyrefly/dataclasses_hash.toml b/conformance/results/pyrefly/dataclasses_hash.toml index 4023cb369..4c89ed9cf 100644 --- a/conformance/results/pyrefly/dataclasses_hash.toml +++ b/conformance/results/pyrefly/dataclasses_hash.toml @@ -5,11 +5,6 @@ errors_diff = """ output = """ ERROR dataclasses_hash.py:17:1-16: Expected a callable, got `None` [not-callable] ERROR dataclasses_hash.py:18:16-22: `DC1` is not assignable to `Hashable` [bad-assignment] -ERROR dataclasses_hash.py:29:12-32: assert_type((self: DC2) -> int, None) failed [assert-type] -ERROR dataclasses_hash.py:43:1-16: Expected a callable, got `None` [not-callable] -ERROR dataclasses_hash.py:44:16-22: `DC3` is not assignable to `Hashable` [bad-assignment] -ERROR dataclasses_hash.py:55:12-32: assert_type((self: DC4) -> int, None) failed [assert-type] -ERROR dataclasses_hash.py:69:12-32: assert_type((self: DC5) -> int, None) failed [assert-type] -ERROR dataclasses_hash.py:85:12-32: assert_type((self: DC6) -> int, None) failed [assert-type] -ERROR dataclasses_hash.py:102:12-32: assert_type((self: DC7) -> int, None) failed [assert-type] +ERROR dataclasses_hash.py:39:1-16: Expected a callable, got `None` [not-callable] +ERROR dataclasses_hash.py:40:16-22: `DC3` is not assignable to `Hashable` [bad-assignment] """ diff --git a/conformance/results/pyright/dataclasses_hash.toml b/conformance/results/pyright/dataclasses_hash.toml index 3bc21bf98..7e05cf570 100644 --- a/conformance/results/pyright/dataclasses_hash.toml +++ b/conformance/results/pyright/dataclasses_hash.toml @@ -5,16 +5,11 @@ dataclasses_hash.py:18:16 - error: Type "DC1" is not assignable to declared type   "DC1" is incompatible with protocol "Hashable"     "__hash__" is an incompatible type       Type "None" is not assignable to type "() -> int" (reportAssignmentType) -dataclasses_hash.py:29:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC2) -> int" (reportAssertTypeFailure) -dataclasses_hash.py:43:1 - error: Object of type "None" cannot be called (reportOptionalCall) -dataclasses_hash.py:44:16 - error: Type "DC3" is not assignable to declared type "Hashable" +dataclasses_hash.py:39:1 - error: Object of type "None" cannot be called (reportOptionalCall) +dataclasses_hash.py:40:16 - error: Type "DC3" is not assignable to declared type "Hashable"   "DC3" is incompatible with protocol "Hashable"     "__hash__" is an incompatible type       Type "None" is not assignable to type "() -> int" (reportAssignmentType) -dataclasses_hash.py:55:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC4) -> int" (reportAssertTypeFailure) -dataclasses_hash.py:69:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC5) -> int" (reportAssertTypeFailure) -dataclasses_hash.py:85:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC6) -> int" (reportAssertTypeFailure) -dataclasses_hash.py:102:13 - error: "assert_type" mismatch: expected "None" but received "(self: DC7) -> int" (reportAssertTypeFailure) """ conformance_automated = "Pass" errors_diff = """ diff --git a/conformance/results/zuban/dataclasses_hash.toml b/conformance/results/zuban/dataclasses_hash.toml index 16ea55f2c..52f7a8faa 100644 --- a/conformance/results/zuban/dataclasses_hash.toml +++ b/conformance/results/zuban/dataclasses_hash.toml @@ -5,18 +5,13 @@ Does not synthesize a `__hash__ = None` class attribute for unhashable dataclass """ errors_diff = """ Line 14: Unexpected errors ['dataclasses_hash.py:14: error: Expression is of type "Callable[[object], int]", not "None" [misc]'] -Line 40: Unexpected errors ['dataclasses_hash.py:40: error: Expression is of type "Callable[[object], int]", not "None" [misc]'] +Line 36: Unexpected errors ['dataclasses_hash.py:36: error: Expression is of type "Callable[[object], int]", not "None" [misc]'] """ output = """ dataclasses_hash.py:14: error: Expression is of type "Callable[[object], int]", not "None" [misc] dataclasses_hash.py:17: error: "DC1" has no attribute "__hash__" [attr-defined] dataclasses_hash.py:18: error: Incompatible types in assignment (expression has type "DC1", variable has type "Hashable") [assignment] -dataclasses_hash.py:29: error: Expression is of type "Callable[[object], int]", not "None" [misc] -dataclasses_hash.py:40: error: Expression is of type "Callable[[object], int]", not "None" [misc] -dataclasses_hash.py:43: error: "DC3" has no attribute "__hash__" [attr-defined] -dataclasses_hash.py:44: error: Incompatible types in assignment (expression has type "DC3", variable has type "Hashable") [assignment] -dataclasses_hash.py:55: error: Expression is of type "Callable[[object], int]", not "None" [misc] -dataclasses_hash.py:69: error: Expression is of type "Callable[[object], int]", not "None" [misc] -dataclasses_hash.py:85: error: Expression is of type "Callable[[DC6], int]", not "None" [misc] -dataclasses_hash.py:102: error: Expression is of type "Callable[[object], int]", not "None" [misc] +dataclasses_hash.py:36: error: Expression is of type "Callable[[object], int]", not "None" [misc] +dataclasses_hash.py:39: error: "DC3" has no attribute "__hash__" [attr-defined] +dataclasses_hash.py:40: error: Incompatible types in assignment (expression has type "DC3", variable has type "Hashable") [assignment] """ diff --git a/conformance/tests/dataclasses_hash.py b/conformance/tests/dataclasses_hash.py index 7bb68eb43..f2d441d68 100644 --- a/conformance/tests/dataclasses_hash.py +++ b/conformance/tests/dataclasses_hash.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass -from typing import Hashable, assert_type +from typing import Callable, Hashable, assert_type @dataclass @@ -23,11 +23,7 @@ class DC2: a: int -# Because `DC2` is frozen, type checkers should synthesize -# a callable `__hash__` method for it, and therefore should -# emit a diagnostic here: -assert_type(DC2.__hash__, None) # E - +dc2_hash: Callable[..., int] = DC2.__hash__ # OK DC2(0).__hash__() # OK v2: Hashable = DC2(0) # OK @@ -49,11 +45,7 @@ class DC4: a: int -# Because `DC4` is frozen, type checkers should synthesize -# a callable `__hash__` method for it, and therefore should -# emit a diagnostic here: -assert_type(DC4.__hash__, None) # E - +dc4_hash: Callable[..., int] = DC4.__hash__ # OK DC4(0).__hash__() # OK v4: Hashable = DC4(0) # OK @@ -63,11 +55,7 @@ class DC5: a: int -# Type checkers should synthesize a callable `__hash__` -# method for `DC5` due to `unsafe_hash=True`, and therefore -# should emit a diagnostic here: -assert_type(DC5.__hash__, None) # E - +dc5_hash: Callable[..., int] = DC5.__hash__ # OK DC5(0).__hash__() # OK v5: Hashable = DC5(0) # OK @@ -80,10 +68,7 @@ def __hash__(self) -> int: return 0 -# Type checkers should respect the manually defined `__hash__` -# method for `DC6`, and therefore should emit a diagnostic here: -assert_type(DC6.__hash__, None) # E - +dc6_hash: Callable[..., int] = DC6.__hash__ # OK DC6(0).__hash__() # OK v6: Hashable = DC6(0) # OK @@ -96,10 +81,6 @@ def __eq__(self, other) -> bool: return self.a == other.a -# Because `DC7` is frozen, type checkers should synthesize -# a callable `__hash__` method for it, and therefore should -# emit a diagnostic here: -assert_type(DC7.__hash__, None) # E - +dc7_hash: Callable[..., int] = DC7.__hash__ # OK DC7(0).__hash__() # OK v7: Hashable = DC7(0) # OK