From 56ad933f6d430a8081ebc469ea18f10f947db7d2 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Wed, 21 Oct 2026 06:45:21 -0700 Subject: [PATCH] Return boolean literals directly from truthiness helpers Teach true_only and false_only to produce the single possible boolean literal directly, just as false_only already does for empty strings and integer zero. Truthiness checks and boolean operators can then stop expanding bool into a union before asking these helpers to restrict it. Authored by Codex --- mypy/checker.py | 5 ++--- mypy/checkexpr.py | 12 +++++------- mypy/test/testtypes.py | 2 ++ mypy/typeops.py | 4 ++++ 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 64852cb724f43..e3aee8fb0e5cf 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6866,12 +6866,11 @@ def find_isinstance_check_helper( # Restrict the type of the variable to True-ish/False-ish in the if and else branches # respectively - original_vartype = self.lookup_type(node) + vartype = self.lookup_type(node) if in_boolean_context: # We don't check `:=` values in expressions like `(a := A())`, # because they produce two error messages. - self.check_for_truthy_type(original_vartype, node) - vartype = try_expanding_sum_type_to_union(original_vartype, "builtins.bool") + self.check_for_truthy_type(vartype, node) if_map = {node: true_only(vartype)} else_map = {node: false_only(vartype)} diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 769e37ac97ee0..0485666c91335 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -159,7 +159,6 @@ is_literal_type_like, make_simplified_union, true_only, - try_expanding_sum_type_to_union, try_getting_str_literals, tuple_fallback, type_object_type, @@ -4457,7 +4456,6 @@ def check_boolean_op(self, e: OpExpr) -> Type: # '[1] or []' are inferred correctly. ctx = self.type_context[-1] left_type = self.accept(e.left, ctx) - expanded_left_type = try_expanding_sum_type_to_union(left_type, "builtins.bool") assert e.op in ("and", "or") # Checked by visit_op_expr @@ -4493,7 +4491,7 @@ def check_boolean_op(self, e: OpExpr) -> Type: self.msg.unreachable_right_operand(e.op, e.right) right_type = self.analyze_cond_branch( - right_map, e.right, self._combined_context(expanded_left_type) + right_map, e.right, self._combined_context(left_type) ) if left_unreachable and right_unreachable: @@ -4507,11 +4505,11 @@ def check_boolean_op(self, e: OpExpr) -> Type: return right_type if e.op == "and": - restricted_left_type = false_only(expanded_left_type) - result_is_left = not expanded_left_type.can_be_true + restricted_left_type = false_only(left_type) + result_is_left = not left_type.can_be_true elif e.op == "or": - restricted_left_type = true_only(expanded_left_type) - result_is_left = not expanded_left_type.can_be_false + restricted_left_type = true_only(left_type) + result_is_left = not left_type.can_be_false if isinstance(restricted_left_type, UninhabitedType): # The left operand can never be the result diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index b287e82b3d4af..03b6f1def084d 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -540,6 +540,7 @@ def test_true_only_of_instance(self) -> None: assert_type(Instance, to) # The original class still can be false assert self.fx.a.can_be_false + assert_equal(true_only(self.fx.bool_type), self.fx.lit_true) def test_true_only_of_union(self) -> None: tup_type = self.tuple(AnyType(TypeOfAny.special_form)) @@ -579,6 +580,7 @@ def test_false_only_of_instance(self) -> None: assert_type(Instance, fo) # The original class still can be true assert self.fx.a.can_be_true + assert_equal(false_only(self.fx.bool_type), self.fx.lit_false) def test_false_only_of_union(self) -> None: with state.strict_optional_set(True): diff --git a/mypy/typeops.py b/mypy/typeops.py index 3035a15e21199..8a4e913886b6f 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -841,6 +841,8 @@ def true_only(t: Type) -> ProperType: new_items = [true_only(item) for item in t.items] can_be_true_items = [item for item in new_items if item.can_be_true] return make_simplified_union(can_be_true_items, line=t.line, column=t.column) + elif isinstance(t, Instance) and t.type.fullname == "builtins.bool": + return LiteralType(True, fallback=t) else: ret_type = _get_type_method_ret_type(t, name="__bool__") or _get_type_method_ret_type( t, name="__len__" @@ -880,6 +882,8 @@ def false_only(t: Type) -> ProperType: return LiteralType("", fallback=t) elif isinstance(t, Instance) and t.type.fullname == "builtins.int": return LiteralType(0, fallback=t) + elif isinstance(t, Instance) and t.type.fullname == "builtins.bool": + return LiteralType(False, fallback=t) else: ret_type = _get_type_method_ret_type(t, name="__bool__") or _get_type_method_ret_type( t, name="__len__"