Skip to content

[1.20 regression] loss of type inference of chained-comparison operands #21149

@TTsangSC

Description

@TTsangSC

Bug Report

My repo1 has a line of code which uses a chained comparison to check if two str | None operands are equal and are not None (see example below). Said code failed type-checking in yesterday's pipeline which pulled the latest mypy from PyPI. Apparently, since 1.20.02 the a argument is no longer narrowed to a str.

To Reproduce

Gist URL: https://gist.github.com/mypy-play/dc825cee3a47c4b77265efb1e253ec02

from typing import reveal_type


def some_func(a: str | None = None, b: str | None = None) -> None:
    if None is not a == b:
        reveal_type(a)
        reveal_type(b)
        print(a + b)  # Stand-in for some code using them as strings
    else:
        ...  # Some other processing happens here

Expected Behavior

a should narrow to str, and also b by virtue of its equality with a.3

Actual Behavior

Below are the mypy Playground outputs:

"mypy master branch"

main.py:6: note: Revealed type is "str | None"
main.py:7: note: Revealed type is "str | None"
main.py:8: error: Unsupported operand types for + ("str" and "None")  [operator]
main.py:8: error: Unsupported left operand type for + ("None")  [operator]
main.py:8: note: Both left and right operands are unions
Found 2 errors in 1 file (checked 1 source file)

"mypy latest (1.20.0)"

main.py:6: note: Revealed type is "builtins.str"
main.py:7: note: Revealed type is "builtins.str | None"
main.py:8: error: Unsupported operand types for + ("str" and "None")  [operator]
main.py:8: note: Right operand is of type "str | None"
Found 1 error in 1 file (checked 1 source file)

Both versions failed to narrow b, but master didn't infer anything about a either. I'm probably out of my depth here, but I'm suspecting that the chained comparison is incorrectly parsed in the new version into something equivalent to None is not (a == b), instead of the correct (None is not a) and (a == b), leading to the complete loss of type narrowing.

Your Environment

For the minimal reproducible example

  • Mypy version used: master (on Playground)
  • Mypy command-line flags: nil
  • Mypy configuration options from mypy.ini (and other config files): nil
  • Python version used: 3.14

For the aforementioned failed pipeline

  • Mypy version used: 1.20.0
  • Mypy command-line flags: nil
  • Mypy configuration options from mypy.ini (and other config files): ignore_missing_imports = true plus misc. options for file in-/ex-clusion
  • Python version used: 3.14.0

Footnotes

  1. Provided for context only since the issue template encourages so: If the project you encountered the issue in is open source, please provide a link to the project.

  2. Note that I failed to replicate the differing behaviors between 1.19 and 1.20 on mypy Playground. Instead, "mypy latest (1.20.0)" retained the behavior from 1.19, while "mypy master branch" replicated the narrowing failure I saw with 1.20.0 in the above pipeline and on my local machine.

  3. As noted in Actual Behavior, b is not narrowed in any recent version. Maybe mypy is trying to be more conservative in the narrowing, accommodating for the off-chance of some str subclass overriding .__eq__() to somehow compare to True with None... ? May have to do with (the discussion around) Plugin interface for type narrowing on == comparisons #10708.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrong

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions