python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.23k stars 2.78k forks source link

ternary operator for side-effect with max #17803

Open serjflint opened 18 hours ago

serjflint commented 18 hours ago

Bug Report

Mypy fails to narrow Optional with ternary. To me it seems similar to https://github.com/python/mypy/issues/5160

To Reproduce

max_similarity = None
average_similarity = 1.0
max_similarity = average_similarity if max_similarity is None else max(average_similarity, max_similarity)  

https://mypy-play.net/?mypy=1.11.2&python=3.8&flags=strict&gist=80aa4ec83bdb4ea7b354f4daa41a6bd6

Expected Behavior

max_similarity = None
average_similarity = 1.0
if max_similarity is None:
    max_similarity = average_similarity
else:
    max_similarity = max(average_similarity, max_similarity)

No errors like in https://mypy-play.net/?mypy=1.11.2&python=3.8&flags=strict&gist=16b2794c5c845bf288c81dd6853569bb

Actual Behavior

https://gist.github.com/serjflint/1b6ae0c2a94fb2774496d6779b52724f

Output ```bash main.py:3: error: No overload variant of "max" matches argument types "float", "None" [call-overload] main.py:3: note: Possible overload variants: main.py:3: note: def [SupportsRichComparisonT] max(SupportsRichComparisonT, SupportsRichComparisonT, /, *_args: SupportsRichComparisonT, key: None = ...) -> SupportsRichComparisonT main.py:3: note: def [_T] max(_T, _T, /, *_args: _T, key: Callable[[_T], Union[SupportsDunderLT[Any], SupportsDunderGT[Any]]]) -> _T main.py:3: note: def [SupportsRichComparisonT] max(Iterable[SupportsRichComparisonT], /, *, key: None = ...) -> SupportsRichComparisonT main.py:3: note: def [_T] max(Iterable[_T], /, *, key: Callable[[_T], Union[SupportsDunderLT[Any], SupportsDunderGT[Any]]]) -> _T main.py:3: note: def [SupportsRichComparisonT, _T] max(Iterable[SupportsRichComparisonT], /, *, key: None = ..., default: _T) -> Union[SupportsRichComparisonT, _T] main.py:3: note: def [_T1, _T2] max(Iterable[_T1], /, *, key: Callable[[_T1], Union[SupportsDunderLT[Any], SupportsDunderGT[Any]]], default: _T2) -> Union[_T1, _T2] Found 1 error in 1 file (checked 1 source file) ```

Your Environment

brianschubert commented 1 hour ago

The problem here is that max_similarity has type None, which can't be narrowed into anything meaningful.

If you give max_similarity a type that can be narrowed, like float | None, then mypy will narrow the type inside the conditional expression:

max_similarity: float | None = None
average_similarity = 1.0
max_similarity = average_similarity if max_similarity is None else max(average_similarity, max_similarity)

This passes with no errors (playground link)

The reason that the second version "works" is because mypy can statically determine that the else branch is unreachable, so it doesn't visit it at all. You can verify this enabling --warn-unreachable, or by adding a type error or a reaveal_type to the else branch:

max_similarity = None
average_similarity = 1.0
if max_similarity is None or input():
    max_similarity = average_similarity
else:                            
    a: str = 1                   # E: Statement is unreachable  [unreachable]
    reveal_type(max_similarity)  # no note
    max_similarity = max(average_similarity, max_similarity)

(playground link).

See also https://mypy.readthedocs.io/en/stable/common_issues.html#unreachable-code