From ed973d32c036771738c86bb0207909be753a9781 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 15 Oct 2025 00:04:18 +0200 Subject: [PATCH 01/15] Emit `redundant-expr` warnings for if statements that are always true or always false. --- mypy/checker.py | 8 +++++-- test-data/unit/check-isinstance.test | 32 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 3bee7b633339..92544932d787 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5019,12 +5019,16 @@ def visit_if_stmt(self, s: IfStmt) -> None: if_map, else_map = self.find_isinstance_check(e) - # XXX Issue a warning if condition is always False? + if codes.REDUNDANT_EXPR in self.options.enabled_error_codes: + if if_map is None: + self.msg.redundant_condition_in_if(False, e) + if else_map is None: + self.msg.redundant_condition_in_if(True, e) + with self.binder.frame_context(can_skip=True, fall_through=2): self.push_type_map(if_map, from_assignment=False) self.accept(b) - # XXX Issue a warning if condition is always True? self.push_type_map(else_map, from_assignment=False) with self.binder.frame_context(can_skip=False, fall_through=2): diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 5043d5422108..da65b1f6c4da 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3033,3 +3033,35 @@ if isinstance(a, B): c = a [builtins fixtures/isinstance.pyi] + +[case testRedundantExpressionWarningsForIfStatements] +# flags: --enable-error-code=redundant-expr +from typing import Literal + +if True: # E: If condition is always true + ... + +if False: # E: If condition is always false + ... + +class W: + ... +if W(): + ... + +class X: + def __bool__(self) -> bool: ... +if X(): + ... + +class Y: + def __bool__(self) -> Literal[True]: ... +if Y(): # E: If condition is always true + ... + +class Z: + def __bool__(self) -> Literal[False]: ... +if Z(): # E: If condition is always false + ... + +[builtins fixtures/bool.pyi] From c38736833810fe41b6873f38fb54d1abbf121590 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 15 Oct 2025 01:06:15 +0200 Subject: [PATCH 02/15] The same for while statements, but only for always false. --- mypy/checker.py | 9 ++++++--- mypy/messages.py | 3 +++ mypy/nodes.py | 15 ++++++++++++--- test-data/unit/check-isinstance.test | 14 ++++++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 92544932d787..9d3a64e60484 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5021,8 +5021,11 @@ def visit_if_stmt(self, s: IfStmt) -> None: if codes.REDUNDANT_EXPR in self.options.enabled_error_codes: if if_map is None: - self.msg.redundant_condition_in_if(False, e) - if else_map is None: + if s.while_stmt: + self.msg.redundant_condition_in_while(e) + else: + self.msg.redundant_condition_in_if(False, e) + if else_map is None and not s.while_stmt: self.msg.redundant_condition_in_if(True, e) with self.binder.frame_context(can_skip=True, fall_through=2): @@ -5037,7 +5040,7 @@ def visit_if_stmt(self, s: IfStmt) -> None: def visit_while_stmt(self, s: WhileStmt) -> None: """Type check a while statement.""" - if_stmt = IfStmt([s.expr], [s.body], None) + if_stmt = IfStmt([s.expr], [s.body], None, while_stmt=True) if_stmt.set_line(s) self.accept_loop(if_stmt, s.else_body, exit_condition=s.expr) diff --git a/mypy/messages.py b/mypy/messages.py index c6378c264757..b133db112386 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2091,6 +2091,9 @@ def redundant_condition_in_comprehension(self, truthiness: bool, context: Contex def redundant_condition_in_if(self, truthiness: bool, context: Context) -> None: self.redundant_expr("If condition", truthiness, context) + def redundant_condition_in_while(self, context: Context) -> None: + self.redundant_expr("While condition", False, context) + def redundant_expr(self, description: str, truthiness: bool, context: Context) -> None: self.fail( f"{description} is always {str(truthiness).lower()}", diff --git a/mypy/nodes.py b/mypy/nodes.py index 040f3fc28dce..931805dae0ff 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1792,19 +1792,28 @@ def accept(self, visitor: StatementVisitor[T]) -> T: class IfStmt(Statement): - __slots__ = ("expr", "body", "else_body") + __slots__ = ("expr", "body", "else_body", "while_stmt") - __match_args__ = ("expr", "body", "else_body") + __match_args__ = ("expr", "body", "else_body", "while_stmt") expr: list[Expression] body: list[Block] else_body: Block | None + while_stmt: bool # Is the if statement a converted while statement? - def __init__(self, expr: list[Expression], body: list[Block], else_body: Block | None) -> None: + def __init__( + self, + expr: list[Expression], + body: list[Block], + else_body: Block | None, + *, + while_stmt: bool = False, + ) -> None: super().__init__() self.expr = expr self.body = body self.else_body = else_body + self.while_stmt = while_stmt def accept(self, visitor: StatementVisitor[T]) -> T: return visitor.visit_if_stmt(self) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index da65b1f6c4da..c28ab4b0eb6f 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3065,3 +3065,17 @@ if Z(): # E: If condition is always false ... [builtins fixtures/bool.pyi] + +[case testRedundantExpressionWarningsForWhileStatements] +# flags: --enable-error-code=redundant-expr + +x = None +while True: + if x: + break + x = True + +while False: # E: While condition is always false + ... + +[builtins fixtures/bool.pyi] From 7031574901be708b5f33b63f9c608ffbe6aa3b85 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 15 Oct 2025 02:36:07 +0200 Subject: [PATCH 03/15] Do not emit `redundant-expr` warnings for `if TYPE_CHECKING` statements. --- mypy/checker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9d3a64e60484..97fca52a3b18 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5019,7 +5019,10 @@ def visit_if_stmt(self, s: IfStmt) -> None: if_map, else_map = self.find_isinstance_check(e) - if codes.REDUNDANT_EXPR in self.options.enabled_error_codes: + if codes.REDUNDANT_EXPR in self.options.enabled_error_codes and not ( + isinstance(e, NameExpr) + and e.fullname in ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING") + ): if if_map is None: if s.while_stmt: self.msg.redundant_condition_in_while(e) From f59720d2c0055e626d62222ac5924a663600f3f6 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Wed, 15 Oct 2025 16:31:32 +0200 Subject: [PATCH 04/15] Use `refers_to_fullname` to detect TYPE_CHECKING` usages. --- mypy/checker.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 97fca52a3b18..eadfc721bc14 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5020,8 +5020,9 @@ def visit_if_stmt(self, s: IfStmt) -> None: if_map, else_map = self.find_isinstance_check(e) if codes.REDUNDANT_EXPR in self.options.enabled_error_codes and not ( - isinstance(e, NameExpr) - and e.fullname in ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING") + refers_to_fullname( + e, ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING") + ) ): if if_map is None: if s.while_stmt: From 89d911ca6c14d5d900f52a9765364eeef3658502 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Thu, 16 Oct 2025 23:33:12 +0200 Subject: [PATCH 05/15] Avoid annoying redundant_expr warnings for exhaustiveness checks. --- mypy/checker.py | 35 ++++++++++++++++++++++ test-data/unit/check-isinstance.test | 45 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index eadfc721bc14..38d88aca70d2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5042,6 +5042,41 @@ def visit_if_stmt(self, s: IfStmt) -> None: if s.else_body: self.accept(s.else_body) + def _visit_if_stmt_redundant_expr_helper( + self, stmt: IfStmt, expr: Expression, body: Body, if_map, else_map + ) -> None: + + if codes.REDUNDANT_EXPR not in self.options.enabled_error_codes: + return + if refers_to_fullname( + expr, ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING") + ): + return + + if if_map is None: + if stmt.while_stmt: + self.msg.redundant_condition_in_while(expr) + else: + self.msg.redundant_condition_in_if(False, expr) + + if else_map is None and not stmt.while_stmt: + if isinstance(body.body[0], ReturnStmt): + return + if (else_body := stmt.else_body) is not None: + s = else_body.body[0] + if isinstance(s, AssertStmt) and is_false_literal(s.expr): + return + if isinstance(s, RaiseStmt): + return + elif isinstance(s.expr, CallExpr): + with self.expr_checker.msg.filter_errors(filter_revealed_type=True): + typ = self.expr_checker.accept( + s.expr, allow_none_return=True, always_allow_any=True + ) + if isinstance(get_proper_type(typ), UninhabitedType): + return + self.msg.redundant_condition_in_if(True, expr) + def visit_while_stmt(self, s: WhileStmt) -> None: """Type check a while statement.""" if_stmt = IfStmt([s.expr], [s.body], None, while_stmt=True) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index c28ab4b0eb6f..18b43da5760c 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3079,3 +3079,48 @@ while False: # E: While condition is always false ... [builtins fixtures/bool.pyi] + +[case testNoRedundantExpressionWarningsForExhaustivenessChecks] +# flags: --enable-error-code=redundant-expr +from typing import Literal, Never, NoReturn + +def assert_never(x: Never) -> NoReturn: + ... +def f1(x: Literal[1, 2]) -> str: + if x == 1: + y = "a" + elif x == 2: + y = "b" + else: + assert_never(x) + return y + +class ValueError: ... +def f2(x: Literal[1]) -> str: + if x == 1: + y = "a" + else: + raise ValueError + return y + +def f3(x: Literal[1]) -> str: + if x == 1: + y = "a" + else: + assert False + return y + +def f4(x: Literal[1, 2]) -> int: + if x == 1: + return 1 + if x == 2: + return 2 + +def f5(x: Literal[1, 2]) -> str: + if x == 1: + y = "a" + elif x == 2: # E: If condition is always true + y = "b" + return y + +[builtins fixtures/bool.pyi] From 83348a334b42332f22a48c3429f8a2d6abb411c4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 21:34:46 +0000 Subject: [PATCH 06/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 38d88aca70d2..86b76f43be39 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5048,9 +5048,7 @@ def _visit_if_stmt_redundant_expr_helper( if codes.REDUNDANT_EXPR not in self.options.enabled_error_codes: return - if refers_to_fullname( - expr, ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING") - ): + if refers_to_fullname(expr, ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING")): return if if_map is None: From 08e2cb728a643e5d56916937af6f2ee8133a2894 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 17 Oct 2025 12:22:01 +0200 Subject: [PATCH 07/15] fix last commit --- mypy/checker.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 86b76f43be39..3cc17b86624a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5019,18 +5019,9 @@ def visit_if_stmt(self, s: IfStmt) -> None: if_map, else_map = self.find_isinstance_check(e) - if codes.REDUNDANT_EXPR in self.options.enabled_error_codes and not ( - refers_to_fullname( - e, ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING") - ) - ): - if if_map is None: - if s.while_stmt: - self.msg.redundant_condition_in_while(e) - else: - self.msg.redundant_condition_in_if(False, e) - if else_map is None and not s.while_stmt: - self.msg.redundant_condition_in_if(True, e) + self._visit_if_stmt_redundant_expr_helper( + stmt=s, expr=e, body=b, if_map=if_map, else_map=else_map + ) with self.binder.frame_context(can_skip=True, fall_through=2): self.push_type_map(if_map, from_assignment=False) @@ -5043,7 +5034,7 @@ def visit_if_stmt(self, s: IfStmt) -> None: self.accept(s.else_body) def _visit_if_stmt_redundant_expr_helper( - self, stmt: IfStmt, expr: Expression, body: Body, if_map, else_map + self, stmt: IfStmt, expr: Expression, body: Block, if_map: TypeMap, else_map: TypeMap ) -> None: if codes.REDUNDANT_EXPR not in self.options.enabled_error_codes: @@ -5066,7 +5057,7 @@ def _visit_if_stmt_redundant_expr_helper( return if isinstance(s, RaiseStmt): return - elif isinstance(s.expr, CallExpr): + elif isinstance(s, ExpressionStmt) and isinstance(s.expr, CallExpr): with self.expr_checker.msg.filter_errors(filter_revealed_type=True): typ = self.expr_checker.accept( s.expr, allow_none_return=True, always_allow_any=True From 491a1e7147398741f399fd5ed100d3ad438a0c35 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 17 Oct 2025 19:56:00 +0200 Subject: [PATCH 08/15] Avoid annoying redundant_expr warnings for dynamic type checks. --- mypy/checker.py | 43 +++++++++++++++++----------- test-data/unit/check-isinstance.test | 4 +++ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 3cc17b86624a..6a62001e9da5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5036,35 +5036,44 @@ def visit_if_stmt(self, s: IfStmt) -> None: def _visit_if_stmt_redundant_expr_helper( self, stmt: IfStmt, expr: Expression, body: Block, if_map: TypeMap, else_map: TypeMap ) -> None: + """Emits `redundant-expr` errors for if statements that are always true or always false. + + We try to avoid emitting such errors if the redundancy seems to be intended as part of + dynamic type or exhaustiveness checking (risking to miss some uninteded redundant if + statements). + """ if codes.REDUNDANT_EXPR not in self.options.enabled_error_codes: return if refers_to_fullname(expr, ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING")): return + def _filter(body: Block | None) -> bool: + if body is None: + return False + s = body.body[0] + if isinstance(s, AssertStmt) and is_false_literal(s.expr): + return True + if isinstance(s, RaiseStmt): + return True + elif isinstance(s, ExpressionStmt) and isinstance(s.expr, CallExpr): + with self.expr_checker.msg.filter_errors(filter_revealed_type=True): + typ = self.expr_checker.accept( + s.expr, allow_none_return=True, always_allow_any=True + ) + if isinstance(get_proper_type(typ), UninhabitedType): + return True + return False + if if_map is None: if stmt.while_stmt: self.msg.redundant_condition_in_while(expr) - else: + elif not _filter(body): self.msg.redundant_condition_in_if(False, expr) if else_map is None and not stmt.while_stmt: - if isinstance(body.body[0], ReturnStmt): - return - if (else_body := stmt.else_body) is not None: - s = else_body.body[0] - if isinstance(s, AssertStmt) and is_false_literal(s.expr): - return - if isinstance(s, RaiseStmt): - return - elif isinstance(s, ExpressionStmt) and isinstance(s.expr, CallExpr): - with self.expr_checker.msg.filter_errors(filter_revealed_type=True): - typ = self.expr_checker.accept( - s.expr, allow_none_return=True, always_allow_any=True - ) - if isinstance(get_proper_type(typ), UninhabitedType): - return - self.msg.redundant_condition_in_if(True, expr) + if not (isinstance(body.body[0], ReturnStmt) or _filter(stmt.else_body)): + self.msg.redundant_condition_in_if(True, expr) def visit_while_stmt(self, s: WhileStmt) -> None: """Type check a while statement.""" diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 18b43da5760c..b540e5d82bd8 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3123,4 +3123,8 @@ def f5(x: Literal[1, 2]) -> str: y = "b" return y +def f6(x: Literal[1]) -> None: + if x != 1: + raise ValueError + [builtins fixtures/bool.pyi] From c2fec838c697e12076ebfd14232b4d4d29399e88 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 17 Oct 2025 19:58:55 +0200 Subject: [PATCH 09/15] fix typo --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6a62001e9da5..123ea9c907a9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5039,7 +5039,7 @@ def _visit_if_stmt_redundant_expr_helper( """Emits `redundant-expr` errors for if statements that are always true or always false. We try to avoid emitting such errors if the redundancy seems to be intended as part of - dynamic type or exhaustiveness checking (risking to miss some uninteded redundant if + dynamic type or exhaustiveness checking (risking to miss some unintended redundant if statements). """ From 968d94d379e3f5b124bff4073fdb939e6fba73f8 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 20 Oct 2025 09:32:27 +0200 Subject: [PATCH 10/15] Use `infer_condition_value` to detect `TYPE_CHECKING`. --- mypy/checker.py | 3 ++- test-data/unit/check-isinstance.test | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 123ea9c907a9..349c0ecc09f3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -152,6 +152,7 @@ from mypy.patterns import AsPattern, StarredPattern from mypy.plugin import Plugin from mypy.plugins import dataclasses as dataclasses_plugin +from mypy.reachability import infer_condition_value, MYPY_TRUE, MYPY_FALSE from mypy.scope import Scope from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS @@ -5045,7 +5046,7 @@ def _visit_if_stmt_redundant_expr_helper( if codes.REDUNDANT_EXPR not in self.options.enabled_error_codes: return - if refers_to_fullname(expr, ("typing.TYPE_CHECKING", "typing_extensions.TYPE_CHECKING")): + if infer_condition_value(expr, self.options) in (MYPY_FALSE, MYPY_TRUE): return def _filter(body: Block | None) -> bool: diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index b540e5d82bd8..15afe53e2d84 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3080,6 +3080,16 @@ while False: # E: While condition is always false [builtins fixtures/bool.pyi] +[case testNoRedundantExpressionWarningsForUsagesOfTYPECHECKING] +# flags: --enable-error-code=redundant-expr +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + ... + +if not TYPE_CHECKING: + ... + [case testNoRedundantExpressionWarningsForExhaustivenessChecks] # flags: --enable-error-code=redundant-expr from typing import Literal, Never, NoReturn From 00bedcf2d6a1d370eeff24cbcdc14df2c6431509 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 20 Oct 2025 09:52:13 +0200 Subject: [PATCH 11/15] fix import order --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 349c0ecc09f3..e237a1e4df74 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -152,7 +152,7 @@ from mypy.patterns import AsPattern, StarredPattern from mypy.plugin import Plugin from mypy.plugins import dataclasses as dataclasses_plugin -from mypy.reachability import infer_condition_value, MYPY_TRUE, MYPY_FALSE +from mypy.reachability import MYPY_FALSE, MYPY_TRUE, infer_condition_value from mypy.scope import Scope from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS From eb6516b236e48947592e557b51223eb70f751bd4 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 20 Oct 2025 10:54:47 +0200 Subject: [PATCH 12/15] check all statements of the relevant if or else block instead of only the first one (which allows to call `is_noop_for_reachability`). --- mypy/checker.py | 14 +------------- test-data/unit/check-isinstance.test | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 105f2e0eac1e..81bb67bfa270 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5054,19 +5054,7 @@ def _visit_if_stmt_redundant_expr_helper( def _filter(body: Block | None) -> bool: if body is None: return False - s = body.body[0] - if isinstance(s, AssertStmt) and is_false_literal(s.expr): - return True - if isinstance(s, RaiseStmt): - return True - elif isinstance(s, ExpressionStmt) and isinstance(s.expr, CallExpr): - with self.expr_checker.msg.filter_errors(filter_revealed_type=True): - typ = self.expr_checker.accept( - s.expr, allow_none_return=True, always_allow_any=True - ) - if isinstance(get_proper_type(typ), UninhabitedType): - return True - return False + return all(self.is_noop_for_reachability(s) for s in body.body) if if_map is None: if stmt.while_stmt: diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 15afe53e2d84..4cec0f84ffe6 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3137,4 +3137,21 @@ def f6(x: Literal[1]) -> None: if x != 1: raise ValueError +def f7(x: Literal[1, 2]) -> None: + if x == 1: + y = "a" + elif x == 2: + y = "b" + else: + pass + +def f8(x: Literal[1, 2]) -> None: + if x == 1: + y = "a" + elif x == 2: # E: If condition is always true + y = "b" + else: + pass + y = "c" + [builtins fixtures/bool.pyi] From 03cdec6b7a80035dc94f43a1af86621afd4320d0 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 20 Oct 2025 11:23:31 +0200 Subject: [PATCH 13/15] ignore truthy while statements only if literals are involved --- mypy/checker.py | 9 ++++++--- mypy/messages.py | 4 ++-- test-data/unit/check-isinstance.test | 4 ++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 81bb67bfa270..9e93596cd134 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5058,12 +5058,15 @@ def _filter(body: Block | None) -> bool: if if_map is None: if stmt.while_stmt: - self.msg.redundant_condition_in_while(expr) + self.msg.redundant_condition_in_while(False, expr) elif not _filter(body): self.msg.redundant_condition_in_if(False, expr) - if else_map is None and not stmt.while_stmt: - if not (isinstance(body.body[0], ReturnStmt) or _filter(stmt.else_body)): + if else_map is None: + if stmt.while_stmt: + if not is_true_literal(expr): + self.msg.redundant_condition_in_while(True, expr) + elif not (_filter(stmt.else_body) or isinstance(body.body[0], ReturnStmt)): self.msg.redundant_condition_in_if(True, expr) def visit_while_stmt(self, s: WhileStmt) -> None: diff --git a/mypy/messages.py b/mypy/messages.py index b133db112386..8b1e40fad9a7 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2091,8 +2091,8 @@ def redundant_condition_in_comprehension(self, truthiness: bool, context: Contex def redundant_condition_in_if(self, truthiness: bool, context: Context) -> None: self.redundant_expr("If condition", truthiness, context) - def redundant_condition_in_while(self, context: Context) -> None: - self.redundant_expr("While condition", False, context) + def redundant_condition_in_while(self, truthiness: bool, context: Context) -> None: + self.redundant_expr("While condition", truthiness, context) def redundant_expr(self, description: str, truthiness: bool, context: Context) -> None: self.fail( diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 4cec0f84ffe6..3fccba5f4a54 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3078,6 +3078,10 @@ while True: while False: # E: While condition is always false ... +y = None +while y is None: # E: While condition is always true + ... + [builtins fixtures/bool.pyi] [case testNoRedundantExpressionWarningsForUsagesOfTYPECHECKING] From b8d726e620a80492733fa7a0dac5d37b7bb31b17 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 20 Oct 2025 11:37:45 +0200 Subject: [PATCH 14/15] do not let testRedundantExpressionWarningsForIfStatements use `...` anymore --- test-data/unit/check-isinstance.test | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 3fccba5f4a54..e40d7e53e7a6 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3039,30 +3039,30 @@ if isinstance(a, B): from typing import Literal if True: # E: If condition is always true - ... + x = 1 if False: # E: If condition is always false - ... + x = 1 class W: ... if W(): - ... + x = 1 class X: def __bool__(self) -> bool: ... if X(): - ... + x = 1 class Y: def __bool__(self) -> Literal[True]: ... if Y(): # E: If condition is always true - ... + x = 1 class Z: def __bool__(self) -> Literal[False]: ... if Z(): # E: If condition is always false - ... + x = 1 [builtins fixtures/bool.pyi] From 66f89327fd95d27afb638f7d2538015970a2b02e Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 21 Oct 2025 02:21:11 +0200 Subject: [PATCH 15/15] Check for missing reachability of the (theoretical) else body instead of just asking whether the if body's first statement is a return statement. --- mypy/checker.py | 17 +++++++++++------ test-data/unit/check-isinstance.test | 12 +++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9e93596cd134..26bf836a29f8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5022,13 +5022,12 @@ def visit_if_stmt(self, s: IfStmt) -> None: if_map, else_map = self.find_isinstance_check(e) - self._visit_if_stmt_redundant_expr_helper( - stmt=s, expr=e, body=b, if_map=if_map, else_map=else_map - ) - with self.binder.frame_context(can_skip=True, fall_through=2): self.push_type_map(if_map, from_assignment=False) self.accept(b) + self._visit_if_stmt_redundant_expr_helper( + s, e, b, if_map, else_map, self.binder.frames[-1].unreachable + ) self.push_type_map(else_map, from_assignment=False) @@ -5037,7 +5036,13 @@ def visit_if_stmt(self, s: IfStmt) -> None: self.accept(s.else_body) def _visit_if_stmt_redundant_expr_helper( - self, stmt: IfStmt, expr: Expression, body: Block, if_map: TypeMap, else_map: TypeMap + self, + stmt: IfStmt, + expr: Expression, + body: Block, + if_map: TypeMap, + else_map: TypeMap, + else_reachable: bool, ) -> None: """Emits `redundant-expr` errors for if statements that are always true or always false. @@ -5066,7 +5071,7 @@ def _filter(body: Block | None) -> bool: if stmt.while_stmt: if not is_true_literal(expr): self.msg.redundant_condition_in_while(True, expr) - elif not (_filter(stmt.else_body) or isinstance(body.body[0], ReturnStmt)): + elif not (else_reachable or _filter(stmt.else_body)): self.msg.redundant_condition_in_if(True, expr) def visit_while_stmt(self, s: WhileStmt) -> None: diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index e40d7e53e7a6..e64f9163c4b8 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -3130,12 +3130,14 @@ def f4(x: Literal[1, 2]) -> int: if x == 2: return 2 -def f5(x: Literal[1, 2]) -> str: +def f5(x: Literal[1, 2]) -> int: if x == 1: - y = "a" - elif x == 2: # E: If condition is always true - y = "b" - return y + return 1 + if x == 2: + if bool(): + return 2 + else: + return 3 def f6(x: Literal[1]) -> None: if x != 1: