Repository navigation
[1.20 regression] loss of type inference of chained-comparison operands #21149
Description
Activity
- changed the title
[-]Loss of type inference of chained-comparison operands in 1.20/master[/-][+][1.20 regression] loss of type inference of chained-comparison operands[/+]on Apr 2, 2026 Further tests show the
==to be the main stumbling block:from typing import reveal_type def f1(a: str | None, b: str | None) -> None: if a is not None is not b: reveal_type(a) reveal_type(b) print(a + b) def f2(a: str | None, b: str | None) -> None: if a is not None is not b == a: reveal_type(a) reveal_type(b) print(a + b)
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:$ python -c "import ast; code = 'a is not None is not b == a'; print(ast.dump(ast.parse(code), indent=' '))" Module( body=[ Expr( value=Compare( left=Name(id='a', ctx=Load()), ops=[ IsNot(), IsNot(), Eq()], comparators=[ Constant(value=None), Name(id='b', ctx=Load()), Name(id='a', ctx=Load())]))])
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 changeReacted by Terence TsangThanks for the quick response!
Just for extra context: replacing the
b == awith other comparisons produces various different results:def f3(a: str | None, b: str | None) -> None: if a is not None is not b is b: # Whatever, I just need a cmp that isn't == reveal_type(a) # mypy says "str" reveal_type(b) # mypy says "str | None" print(a + b) def f4(a: str | None, b: str | None) -> None: if a is not None is not b is not a: # Ditto above reveal_type(a) # mypy says "str" reveal_type(b) # mypy says "str" print(a + b)
- added a commit that references this issue
on Apr 2, 2026 - added 2 commits that reference this issue
on 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.
Reacted by Terence TsangThanks for the fixes! Looking forward to the new releases.
- added a commit that references this issue
on Apr 5, 2026 - added a commit that references this issue
on Apr 13, 2026
Bug Report
My repo1 has a line of code which uses a chained comparison to check if two
str | Noneoperands are equal and are notNone(see example below). Said code failed type-checking in yesterday's pipeline which pulled the latestmypyfrom PyPI. Apparently, since 1.20.02 theaargument is no longer narrowed to astr.To Reproduce
Gist URL: https://gist.github.com/mypy-play/dc825cee3a47c4b77265efb1e253ec02
Expected Behavior
ashould narrow tostr, and alsobby virtue of its equality witha.3Actual Behavior
Below are the
mypyPlayground outputs:"mypy master branch"
"mypy latest (1.20.0)"
Both versions failed to narrow
b, butmasterdidn't infer anything aboutaeither. 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 toNone 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.ini(and other config files): nilFor the aforementioned failed pipeline
mypy.ini(and other config files):ignore_missing_imports = trueplus misc. options for file in-/ex-clusionFootnotes
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. ↩