From ccd5179f3f2884e929ac488de92a31c9453d82c3 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 23 Aug 2025 23:49:50 +0200 Subject: [PATCH 1/5] `--strict-equality` for `None` I'm curious to see what the Mypy primer thinks of it... --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 88b3005b1376..f4d5e558861c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3817,7 +3817,7 @@ def dangerous_comparison( # # TODO: find a way of disabling the check only for types resulted from the expansion. return False - if isinstance(left, NoneType) or isinstance(right, NoneType): + if isinstance(left, NoneType) and isinstance(right, NoneType): return False if isinstance(left, UnionType) and isinstance(right, UnionType): left = remove_optional(left) From e37d706aa2620d91a589407c5d0e7f2e0e5cd913 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 27 Aug 2025 08:49:35 +0200 Subject: [PATCH 2/5] add option `--strict-equality-for-none` --- docs/source/config_file.rst | 10 ++++++++- docs/source/error_code_list2.rst | 23 +++++++++++++++++++++ mypy/checkexpr.py | 14 ++++++++----- mypy/main.py | 11 +++++++++- mypy/options.py | 4 ++++ test-data/unit/check-expressions.test | 29 +++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 7 deletions(-) diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index b4f134f26cb1..934e465a7c23 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -834,7 +834,15 @@ section of the command line docs. :default: False Prohibit equality checks, identity checks, and container checks between - non-overlapping types. + non-overlapping types (except ``None``). + +.. confval:: strict_equality_for_none + + :type: boolean + :default: False + + Include ``None`` in strict equality checks (requires :confval:`strict_equality` + to be activated). .. confval:: strict_bytes diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 784c2ad72819..c48971bb99bb 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -145,6 +145,29 @@ literal: def is_magic(x: bytes) -> bool: return x == b'magic' # OK +:option:`--strict-equality ` does not include comparisons with +``None`` for historical reasons (support for type comments): + +.. code-block:: python + + # mypy: strict-equality + + def is_none(x: str) -> bool: + return x is None # OK + +If you want such checks, you must also activate +:option:`--strict-equality-for-none ` (we might merge +these two options later). + +.. code-block:: python + + # mypy: strict-equality strict-equality-for-none + + def is_none(x: str) -> bool: + # Error: Non-overlapping identity check + # (left operand type: "str", right operand type: "None") + return x is None + .. _code-no-untyped-call: Check that no untyped functions are called [no-untyped-call] diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f4d5e558861c..5d484cfc478d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3721,7 +3721,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type: elif operator == "is" or operator == "is not": right_type = self.accept(right) # validate the right operand sub_result = self.bool_type() - if self.dangerous_comparison(left_type, right_type): + if self.dangerous_comparison(left_type, right_type, identity_check=True): # Show the most specific literal types possible left_type = try_getting_literal(left_type) right_type = try_getting_literal(right_type) @@ -3763,6 +3763,7 @@ def dangerous_comparison( original_container: Type | None = None, seen_types: set[tuple[Type, Type]] | None = None, prefer_literal: bool = True, + identity_check: bool = False, ) -> bool: """Check for dangerous non-overlapping comparisons like 42 == 'no'. @@ -3790,10 +3791,10 @@ def dangerous_comparison( left, right = get_proper_types((left, right)) - # We suppress the error if there is a custom __eq__() method on either - # side. User defined (or even standard library) classes can define this + # We suppress the error for equality and container checks if there is a custom __eq__() + # method on either side. User defined (or even standard library) classes can define this # to return True for comparisons between non-overlapping types. - if custom_special_method(left, "__eq__") or custom_special_method(right, "__eq__"): + if (custom_special_method(left, "__eq__") or custom_special_method(right, "__eq__")) and not identity_check: return False if prefer_literal: @@ -3817,7 +3818,10 @@ def dangerous_comparison( # # TODO: find a way of disabling the check only for types resulted from the expansion. return False - if isinstance(left, NoneType) and isinstance(right, NoneType): + if self.chk.options.strict_equality_for_none: + if isinstance(left, NoneType) and isinstance(right, NoneType): + return False + elif isinstance(left, NoneType) or isinstance(right, NoneType): return False if isinstance(left, UnionType) and isinstance(right, UnionType): left = remove_optional(left) diff --git a/mypy/main.py b/mypy/main.py index 0f70eb41bb14..93cc1a356fc7 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -905,7 +905,16 @@ def add_invertible_flag( "--strict-equality", default=False, strict_flag=True, - help="Prohibit equality, identity, and container checks for non-overlapping types", + help="Prohibit equality, identity, and container checks for non-overlapping types " + "(except `None`)", + group=strictness_group, + ) + + add_invertible_flag( + "--strict-equality-for-none", + default=False, + strict_flag=False, + help="Extend `--strict-equality` for `None` checks", group=strictness_group, ) diff --git a/mypy/options.py b/mypy/options.py index ad4b26cca095..b3dc9639a41d 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -55,6 +55,7 @@ class BuildType: "mypyc", "strict_concatenate", "strict_equality", + "strict_equality_for_none", "strict_optional", "warn_no_return", "warn_return_any", @@ -230,6 +231,9 @@ def __init__(self) -> None: # This makes 1 == '1', 1 in ['1'], and 1 is '1' errors. self.strict_equality = False + # Extend the logic of `scrict_equality` for comparisons with `None`. + self.strict_equality_for_none = False + # Disable treating bytearray and memoryview as subtypes of bytes self.strict_bytes = False diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 33271a3cc04c..ea6eac9a39b3 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2419,6 +2419,35 @@ assert a == c [builtins fixtures/list.pyi] [typing fixtures/typing-full.pyi] +[case testStrictEqualityForNone] +# flags: --strict-equality --strict-equality-for-none + +class A: ... + +def a1(x: A) -> None: + assert x is None # E: Non-overlapping identity check (left operand type: "A", right operand type: "None") +def a2(x: A) -> None: + x is not None # E: Non-overlapping identity check (left operand type: "A", right operand type: "None") +def a3(x: A) -> None: + None == x # E: Non-overlapping equality check (left operand type: "None", right operand type: "A") +def a4(x: list[A]) -> None: + None in x # E: Non-overlapping container check (element type: "None", container item type: "A") + +class B: + def __eq__(self, x: object) -> bool: ... + +def b1(x: B) -> None: + assert x is None # E: Non-overlapping identity check (left operand type: "B", right operand type: "None") +def b2(x: B) -> None: + x is not None # E: Non-overlapping identity check (left operand type: "B", right operand type: "None") +def b3(x: B) -> None: + x == None +def b4(x: list[B]) -> None: + None in x + +[builtins fixtures/list.pyi] +[typing fixtures/typing-full.pyi] + [case testUnimportedHintAny] def f(x: Any) -> None: # E: Name "Any" is not defined \ # N: Did you forget to import it from "typing"? (Suggestion: "from typing import Any") From 5485b6f4301af6724c67e5e277e471d0be190921 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 06:51:08 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 4 +++- mypy/main.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 5d484cfc478d..e9620184a1d9 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -3794,7 +3794,9 @@ def dangerous_comparison( # We suppress the error for equality and container checks if there is a custom __eq__() # method on either side. User defined (or even standard library) classes can define this # to return True for comparisons between non-overlapping types. - if (custom_special_method(left, "__eq__") or custom_special_method(right, "__eq__")) and not identity_check: + if ( + custom_special_method(left, "__eq__") or custom_special_method(right, "__eq__") + ) and not identity_check: return False if prefer_literal: diff --git a/mypy/main.py b/mypy/main.py index 93cc1a356fc7..9f6431376e40 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -906,7 +906,7 @@ def add_invertible_flag( default=False, strict_flag=True, help="Prohibit equality, identity, and container checks for non-overlapping types " - "(except `None`)", + "(except `None`)", group=strictness_group, ) From 7e8cb1c188cfee040c7f8efdd09bebdae128c2a1 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 28 Aug 2025 06:45:30 +0200 Subject: [PATCH 4/5] add option `--strict-equality-for-none` --- docs/source/command_line.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index db2407e17df8..1894ea96db27 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -728,9 +728,22 @@ of the above sections. if text != b'other bytes': # Error: non-overlapping equality check! ... - assert text is not None # OK, check against None is allowed as a special case. + assert text is not None # OK, check against None is allowed for historical reasons. +.. option:: --strict-equality-for-none + + This flag extends :option:`--strict-equality ` for checks + against ``None``: + + .. code-block:: python + + text: str + assert text is not None # Error: non-overlapping identity check! + + Note that :option:`--strict-equality-for-none ` + only works in combination with :option:`--strict-equality `. + .. option:: --strict-bytes By default, mypy treats ``bytearray`` and ``memoryview`` as subtypes of ``bytes`` which From eb73f748d0eba782fa678e8143acaddc685c1a1f Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 28 Aug 2025 22:25:06 +0200 Subject: [PATCH 5/5] remove "historical reason" explanations --- docs/source/command_line.rst | 2 +- docs/source/error_code_list2.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 1894ea96db27..c1b757a00ef2 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -728,7 +728,7 @@ of the above sections. if text != b'other bytes': # Error: non-overlapping equality check! ... - assert text is not None # OK, check against None is allowed for historical reasons. + assert text is not None # OK, check against None is allowed .. option:: --strict-equality-for-none diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index c48971bb99bb..125671bc2bef 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -146,7 +146,7 @@ literal: return x == b'magic' # OK :option:`--strict-equality ` does not include comparisons with -``None`` for historical reasons (support for type comments): +``None``: .. code-block:: python