Skip to content

Commit 618cf55

Browse files
--strict-equality for None (#19718)
Fixes #18386 (at least partly) (edited) In a first test run, in which I included checks against `None` in `--strict-equality`, the Mypy primer gave hundreds of new `comparison-overlap` reports. Many of them seem really helpful (including those for the Mypy source code itself), because it is often hard to tell if non-overlapping `None` checks are just remnants of incomplete refactorings or can handle cases with corrupted data or similar issues. As it was only a little effort, I decided to add the option `--strict-equality-for-none` to Mypy, which is disabled even in `--strict` mode. Other libraries could adjust to this new behaviour if and when they want. If many of them do so, we could eventually enable `--strict-equality-for-none` in `--strict` mode or even merge it with `--strict-equality` later. The remaining new true positives revealed by the Mypy primer are the result of no longer excluding types with custom `__eq__` methods for identity checks (which, in my opinion, makes sense even in case `--strict-equality-for-none` would be rejected). --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 06f97b2 commit 618cf55

File tree

7 files changed

+100
-8
lines changed

7 files changed

+100
-8
lines changed

docs/source/command_line.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,9 +728,22 @@ of the above sections.
728728
if text != b'other bytes': # Error: non-overlapping equality check!
729729
...
730730
731-
assert text is not None # OK, check against None is allowed as a special case.
731+
assert text is not None # OK, check against None is allowed
732732
733733
734+
.. option:: --strict-equality-for-none
735+
736+
This flag extends :option:`--strict-equality <mypy --strict-equality>` for checks
737+
against ``None``:
738+
739+
.. code-block:: python
740+
741+
text: str
742+
assert text is not None # Error: non-overlapping identity check!
743+
744+
Note that :option:`--strict-equality-for-none <mypy --strict-equality-for-none>`
745+
only works in combination with :option:`--strict-equality <mypy --strict-equality>`.
746+
734747
.. option:: --strict-bytes
735748

736749
By default, mypy treats ``bytearray`` and ``memoryview`` as subtypes of ``bytes`` which

docs/source/config_file.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,15 @@ section of the command line docs.
834834
:default: False
835835

836836
Prohibit equality checks, identity checks, and container checks between
837-
non-overlapping types.
837+
non-overlapping types (except ``None``).
838+
839+
.. confval:: strict_equality_for_none
840+
841+
:type: boolean
842+
:default: False
843+
844+
Include ``None`` in strict equality checks (requires :confval:`strict_equality`
845+
to be activated).
838846

839847
.. confval:: strict_bytes
840848

docs/source/error_code_list2.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,29 @@ literal:
145145
def is_magic(x: bytes) -> bool:
146146
return x == b'magic' # OK
147147
148+
:option:`--strict-equality <mypy --strict-equality>` does not include comparisons with
149+
``None``:
150+
151+
.. code-block:: python
152+
153+
# mypy: strict-equality
154+
155+
def is_none(x: str) -> bool:
156+
return x is None # OK
157+
158+
If you want such checks, you must also activate
159+
:option:`--strict-equality-for-none <mypy --strict-equality-for-none>` (we might merge
160+
these two options later).
161+
162+
.. code-block:: python
163+
164+
# mypy: strict-equality strict-equality-for-none
165+
166+
def is_none(x: str) -> bool:
167+
# Error: Non-overlapping identity check
168+
# (left operand type: "str", right operand type: "None")
169+
return x is None
170+
148171
.. _code-no-untyped-call:
149172

150173
Check that no untyped functions are called [no-untyped-call]

mypy/checkexpr.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3721,7 +3721,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
37213721
elif operator == "is" or operator == "is not":
37223722
right_type = self.accept(right) # validate the right operand
37233723
sub_result = self.bool_type()
3724-
if self.dangerous_comparison(left_type, right_type):
3724+
if self.dangerous_comparison(left_type, right_type, identity_check=True):
37253725
# Show the most specific literal types possible
37263726
left_type = try_getting_literal(left_type)
37273727
right_type = try_getting_literal(right_type)
@@ -3763,6 +3763,7 @@ def dangerous_comparison(
37633763
original_container: Type | None = None,
37643764
seen_types: set[tuple[Type, Type]] | None = None,
37653765
prefer_literal: bool = True,
3766+
identity_check: bool = False,
37663767
) -> bool:
37673768
"""Check for dangerous non-overlapping comparisons like 42 == 'no'.
37683769
@@ -3790,10 +3791,12 @@ def dangerous_comparison(
37903791

37913792
left, right = get_proper_types((left, right))
37923793

3793-
# We suppress the error if there is a custom __eq__() method on either
3794-
# side. User defined (or even standard library) classes can define this
3794+
# We suppress the error for equality and container checks if there is a custom __eq__()
3795+
# method on either side. User defined (or even standard library) classes can define this
37953796
# to return True for comparisons between non-overlapping types.
3796-
if custom_special_method(left, "__eq__") or custom_special_method(right, "__eq__"):
3797+
if (
3798+
custom_special_method(left, "__eq__") or custom_special_method(right, "__eq__")
3799+
) and not identity_check:
37973800
return False
37983801

37993802
if prefer_literal:
@@ -3817,7 +3820,10 @@ def dangerous_comparison(
38173820
#
38183821
# TODO: find a way of disabling the check only for types resulted from the expansion.
38193822
return False
3820-
if isinstance(left, NoneType) or isinstance(right, NoneType):
3823+
if self.chk.options.strict_equality_for_none:
3824+
if isinstance(left, NoneType) and isinstance(right, NoneType):
3825+
return False
3826+
elif isinstance(left, NoneType) or isinstance(right, NoneType):
38213827
return False
38223828
if isinstance(left, UnionType) and isinstance(right, UnionType):
38233829
left = remove_optional(left)

mypy/main.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,16 @@ def add_invertible_flag(
903903
"--strict-equality",
904904
default=False,
905905
strict_flag=True,
906-
help="Prohibit equality, identity, and container checks for non-overlapping types",
906+
help="Prohibit equality, identity, and container checks for non-overlapping types "
907+
"(except `None`)",
908+
group=strictness_group,
909+
)
910+
911+
add_invertible_flag(
912+
"--strict-equality-for-none",
913+
default=False,
914+
strict_flag=False,
915+
help="Extend `--strict-equality` for `None` checks",
907916
group=strictness_group,
908917
)
909918

mypy/options.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class BuildType:
5555
"mypyc",
5656
"strict_concatenate",
5757
"strict_equality",
58+
"strict_equality_for_none",
5859
"strict_optional",
5960
"warn_no_return",
6061
"warn_return_any",
@@ -230,6 +231,9 @@ def __init__(self) -> None:
230231
# This makes 1 == '1', 1 in ['1'], and 1 is '1' errors.
231232
self.strict_equality = False
232233

234+
# Extend the logic of `scrict_equality` for comparisons with `None`.
235+
self.strict_equality_for_none = False
236+
233237
# Disable treating bytearray and memoryview as subtypes of bytes
234238
self.strict_bytes = False
235239

test-data/unit/check-expressions.test

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,6 +2419,35 @@ assert a == c
24192419
[builtins fixtures/list.pyi]
24202420
[typing fixtures/typing-full.pyi]
24212421

2422+
[case testStrictEqualityForNone]
2423+
# flags: --strict-equality --strict-equality-for-none
2424+
2425+
class A: ...
2426+
2427+
def a1(x: A) -> None:
2428+
assert x is None # E: Non-overlapping identity check (left operand type: "A", right operand type: "None")
2429+
def a2(x: A) -> None:
2430+
x is not None # E: Non-overlapping identity check (left operand type: "A", right operand type: "None")
2431+
def a3(x: A) -> None:
2432+
None == x # E: Non-overlapping equality check (left operand type: "None", right operand type: "A")
2433+
def a4(x: list[A]) -> None:
2434+
None in x # E: Non-overlapping container check (element type: "None", container item type: "A")
2435+
2436+
class B:
2437+
def __eq__(self, x: object) -> bool: ...
2438+
2439+
def b1(x: B) -> None:
2440+
assert x is None # E: Non-overlapping identity check (left operand type: "B", right operand type: "None")
2441+
def b2(x: B) -> None:
2442+
x is not None # E: Non-overlapping identity check (left operand type: "B", right operand type: "None")
2443+
def b3(x: B) -> None:
2444+
x == None
2445+
def b4(x: list[B]) -> None:
2446+
None in x
2447+
2448+
[builtins fixtures/list.pyi]
2449+
[typing fixtures/typing-full.pyi]
2450+
24222451
[case testUnimportedHintAny]
24232452
def f(x: Any) -> None: # E: Name "Any" is not defined \
24242453
# N: Did you forget to import it from "typing"? (Suggestion: "from typing import Any")

0 commit comments

Comments
 (0)