Repository navigation
[1.20 regression] loss of type inference of chained-comparison operands #21149
Description
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 hereExpected 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 = trueplus misc. options for file in-/ex-clusion - Python version used: 3.14.0
Footnotes
-
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.↩ -
Note that I failed to replicate the differing behaviors between 1.19 and 1.20 on
mypyPlayground. 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. ↩ -
As noted in Actual Behavior,
bis not narrowed in any recent version. Maybemypyis trying to be more conservative in the narrowing, accommodating for the off-chance of somestrsubclass overriding.__eq__()to somehow compare toTruewithNone... ? May have to do with (the discussion around) Plugin interface for type narrowing on==comparisons #10708. ↩
Activity
[-]Loss of type inference of chained-comparison operands in 1.20/master[/-][+][1.20 regression] loss of type inference of chained-comparison operands[/+]TTsangSC commentedon Apr 2, 2026
Further tests show the
==to be the main stumbling block:On
mypyPlayground, "mypy latest (1.20.0)" correctly reports all 4reveal_type()arguments to bestrs, while "mypy master branch" fails to narrow bothaandbinf2()(Gist: https://gist.github.com/mypy-play/b60757b1af709f36b33dd6a8cb66fe14).Just as a sanity check,
==andis notare indeed processed on equal footing as comparators by the language:hauntsaninja commentedon Apr 2, 2026
Thanks for the report! It looks like it is easy to get back to the behaviour of 1.19, where we narrow
a.I'm now looking into if it is easy to narrow
bas well, but that isn't a regression and might be a larger changeTTsangSC commentedon Apr 2, 2026
Thanks for the quick response!
Just for extra context: replacing the
b == awith other comparisons produces various different results:Fix narrowing with chained comparison
Fix narrowing with chained comparison (#21150)
Propagate narrowing within chained comparisons
hauntsaninja commentedon Apr 3, 2026
Thanks for the issues!
1.20.1 will contain a fix for the regression here as well as a fix for the regression involving empty tuple exception handlers.
The next non-patch version of mypy will fix the narrowing behaviour for
b(see #21160)I also added pytest-autoprofile to the list of projects we run ecosystem regression checks on in hauntsaninja/mypy_primer#244, so if future changes to mypy regress something on that project we will learn at the time of making the change.
TTsangSC commentedon Apr 3, 2026
Thanks for the fixes! Looking forward to the new releases.
Fix narrowing with chained comparison (#21150)
Propagate narrowing within chained comparisons (#21160)