Repository: python/mypy (python)

Issue:
[1.19 regression] Ternary expression involving a union with `Any` evaluated as redundant

<!--
If you're not sure whether what you're experiencing is a mypy bug, please see the "Question and Help" form instead.
Please consider:

- checking our common issues page: https://mypy.readthedocs.io/en/stable/common_issues.html
- searching our issue tracker: https://github.com/python/mypy/issues to see if it's already been reported
- asking on gitter chat: https://gitter.im/python/typing
-->

**Bug Report**

<!--
If you're reporting a problem with a specific library function, the typeshed tracker is better suited for this report: https://github.com/python/typeshed/issues

If the project you encountered the issue in is open source, please provide a link to the project.
-->

We recently ran into something in a canary pipeline with mypy 1.19 and it sounded related to https://github.com/python/mypy/issues/20363, but am making this issue separately per https://github.com/python/mypy/issues/20363#issuecomment-3675211938

It looks like `Any` or `Awaitable` may not always be type narrowed correctly when in a union and in a ternary expression?

**To Reproduce**

The following emits `error: If condition is always true  [redundant-expr]`

```python
from typing import Any, Awaitable
x: list[Any | Awaitable]

for y in x:
    z = 1 if isinstance(y, Awaitable) else 2

```


Changing the typing to drop `Awaitable` from the union:

```python
from typing import Any, Awaitable
x: list[Any]

for y in x:
    z = 1 if isinstance(y, Awaitable) else 2

```

or typing the `Awaitable` generic to a specific type:


```python
from typing import Any, Awaitable
x: list[Any|Awaitable[bool]]

for y in x:
    z = 1 if isinstance(y, Awaitable) else 2

```

or breaking up the ternary:

```python
from typing import Any, Awaitable
x: list[Any | Awaitable]

for y in x:
    if isinstance(y, Awaitable):
        z = 1
    else:
        z = 2
```

resolves the error.

We did not see this in 1.18.x


**Expected Behavior**

<!--
How did you expect mypy to behave? It’s fine if you’re not sure your understanding is correct.
Write down what you thought would happen. If you expected no errors, delete this section.
-->

In this case, I would expect there to be some differentiation between `Any` and `Awaitable` such that the conditional is not considered redundant


**Your Environment**

<!-- Include as many relevant details about the environment you experienced the bug in -->

- Mypy version used: 1.19.x
- Mypy command-line flags: 
- Mypy configuration options from `mypy.ini` (and other config files): `redundant-expr`
- Python version used: 3.11+

<!-- You can freely edit this text, please remove all the lines you believe are unnecessary. -->



Patch:
diff --git a/mypy/checker.py b/mypy/checker.py
index f90fc4be41f4d..20e413ce8c804 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -8394,43 +8394,42 @@ def conditional_types(
 
     if isinstance(proper_type, AnyType):
         return proposed_type, current_type
-    elif isinstance(proposed_type, AnyType):
+    if isinstance(proposed_type, AnyType):
         # We don't really know much about the proposed type, so we shouldn't
         # attempt to narrow anything. Instead, we broaden the expr to Any to
         # avoid false positives
         return proposed_type, default
-    elif not any(type_range.is_upper_bound for type_range in proposed_type_ranges) and (
-        # concrete subtypes
-        is_proper_subtype(current_type, proposed_type, ignore_promotions=True)
+    if not any(type_range.is_upper_bound for type_range in proposed_type_ranges):
+        # concrete subtype
+        if is_proper_subtype(current_type, proposed_type, ignore_promotions=True):
+            return default, UninhabitedType()
+
         # structural subtypes
-        or (
-            (
-                isinstance(proposed_type, CallableType)
-                or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol)
+        if (
+            isinstance(proposed_type, CallableType)
+            or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol)
+        ) and is_subtype(current_type, proposed_type, ignore_promotions=True):
+            # Note: It's possible that current_type=`Any | Proto` while proposed_type=`Proto`
+            #  so we cannot return `Never` for the else branch
+            remainder = restrict_subtype_away(
+                current_type,
+                default if default is not None else proposed_type,
+                consider_runtime_isinstance=consider_runtime_isinstance,
             )
-            and is_subtype(current_type, proposed_type, ignore_promotions=True)
-        )
-    ):
-        # Expression is always of one of the types in proposed_type_ranges
-        return default, UninhabitedType()
-    elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True):
+            return default, remainder
+    if not is_overlapping_types(current_type, proposed_type, ignore_promotions=True):
         # Expression is never of any type in proposed_type_ranges
         return UninhabitedType(), default
-    else:
-        # we can only restrict when the type is precise, not bounded
-        proposed_precise_type = UnionType.make_union(
-            [
-                type_range.item
-                for type_range in proposed_type_ranges
-                if not type_range.is_upper_bound
-            ]
-        )
-        remaining_type = restrict_subtype_away(
-            current_type,
-            proposed_precise_type,
-            consider_runtime_isinstance=consider_runtime_isinstance,
-        )
-        return proposed_type, remaining_type
+    # we can only restrict when the type is precise, not bounded
+    proposed_precise_type = UnionType.make_union(
+        [type_range.item for type_range in proposed_type_ranges if not type_range.is_upper_bound]
+    )
+    remaining_type = restrict_subtype_away(
+        current_type,
+        proposed_precise_type,
+        consider_runtime_isinstance=consider_runtime_isinstance,
+    )
+    return proposed_type, remaining_type
 
 
 def conditional_types_to_typemaps(


Test patch:
diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test
index 31b9c526b6e5f..dade18a69a3a7 100644
--- a/test-data/unit/check-errorcodes.test
+++ b/test-data/unit/check-errorcodes.test
@@ -866,6 +866,20 @@ class Foo:
         return 2
 [builtins fixtures/list.pyi]
 
+[case testRedundantExpressionsUnionWithAny]
+# flags: --enable-error-code redundant-expr
+from typing import Any, Awaitable, Union
+ok_list: list[Union[Any, Awaitable]]
+for y in ok_list:
+    z = 1 if isinstance(y, Awaitable) else 2
+
+bad_list: list[Awaitable]
+for y in bad_list:
+    z = 1 if isinstance(y, Awaitable) else 2  # E: If condition is always true  [redundant-expr]
+[builtins fixtures/isinstancelist.pyi]
+[typing fixtures/typing-full.pyi]
+
+
 [case testNamedTupleNameMismatch]
 from typing import NamedTuple