Skip to content

Fix false positive list-item for list literal against union with Hashable - #22381

Open
xh123637 wants to merge 4 commits into
python:masterfrom
xh123637:fix/22342-list-literal-union-false-positive
Open

xh123637 wants to merge 4 commits into
python:masterfrom
xh123637:fix/22342-list-literal-union-false-positive

Conversation

@xh123637

@xh123637 xh123637 commented Nov 6, 2026

Copy link
Copy Markdown

Fix false positive list-item for list literal against union with Hashable

Fixes #22342.

from collections.abc import Hashable, MutableSequence

def f(k: list[bool] | Hashable | MutableSequence[str]) -> None: ...

f(["a", "b"])  # was: List item 0/1 has incompatible type "str"; expected "bool"

Pyright, pyrefly and ty accept the call; mypy reported both items against bool.

Root cause

Union simplification in mypy/constraints.py dropped MutableSequence[str] from
the context union before inferring list[T] — it is a proper subtype of Hashable
(typeshed's MutableSequence doesn't set __hash__ = None). But list does set
__hash__ = None, so the list[T] template cannot match the remaining Hashable
member, leaving list[bool] as the only option and forcing T = bool.

Subtyping is not transitive here: MutableSequence[str] <: Hashable and
list[str] <: MutableSequence[str], but list[str] is not a subtype of Hashable.

Fix

_infer_constraints now keeps the unsimplified union items around, and in the
SUBTYPE_OF + union-context branch it recovers constraint options for items
dropped by simplification — but only when the template can match the dropped item
while every item that absorbed it cannot. That is exactly the "subtyping wasn't
transitive here" case, so normal simplification behavior is unchanged (e.g.
list[int] | Sequence[int] still simplifies, and list[bool] | Hashable still
reports list-item for ["a"]).

How this differs from #22366

#22366 bundled this same constraint-inference recovery together with a second,
unrelated fix (deferred validation of member-access base classes for #22337).
The mypy_primer regressions on that PR — new valid-type errors in operator
and scrapy around self._charm_cls / dict_class / spider_class used as base
classes — all came from the member-base change, not from the constraints change.
This PR contains only the #22342 fix, with no semanal/checker changes, so those
regressions do not apply. The constraint-recovery design follows the analysis
posted by hamarshehmhmd in the issue (who tested the same approach on master).

Tests

New regression test testListLiteralContextWithUnhashableListAndHashableSequence
in test-data/unit/check-inference-context.test with a new fixture
test-data/unit/fixtures/list_hashable.pyi (builtins stub with list.__hash__
as ClassVar[None], since the default fixtures don't model list.__hash__).
It failed on unmodified code with the reported list-item errors and passes
with the fix. It also covers f([True]) (passes) and f([1.5]), which now
errors as arg-type against the whole union with the __hash__ conflict note
instead of list-item vs bool.

Test runs (all with the repo venv):

  • New test: 1 passed (failed before the fix)
  • pytest mypy/test/testcheck.py mypy/test/testtypes.py mypy/test/testsolve.py mypy/test/testinfer.py: 8486 passed, 29 skipped, 7 xfailed
  • Live checks with the built mypy: the issue repro now passes; g(["a"]) with
    list[bool] | Hashable still errors with list-item as before
  • Targeted self-check of mypy/constraints.py: no new errors (same pre-existing
    output as clean master, modulo line shifts); black clean

Note: I used an AI coding assistant while working on this fix, and verified
the change and the tests myself.

xh123637 and others added 2 commits November 6, 2026 15:24
…able

Fixes python#22342.

When a list literal is checked against a union such as
list[bool] | Hashable | MutableSequence[str], union simplification
dropped MutableSequence[str] (a proper subtype of Hashable), and the
list template could not match the remaining Hashable member (list is
unhashable), so inference committed to list[bool] and reported the
items against bool. Constraint inference now recovers options for
union items lost to simplification when the template can match the
dropped item but none of the items that absorbed it can.

I used an AI coding assistant while working on this fix, and verified
the change and the tests myself.
@github-actions

This comment has been minimized.

The recovery in _options_lost_by_simplification added a constraint even
when no simplified union item produced one, which invented a
T = Hashable constraint for [names] against
Hashable | Sequence[Hashable] | dict[Hashable, Hashable] and turned the
previous assignment error into a new list-item error
(pandas/core/indexes/base.py:2143, reported by mypy_primer on python#22381).
Recovery exists to offer an alternative to a (possibly wrong) commitment,
not to invent constraints where the fallback already applies, so skip
recovery when every simplified option is None. The python#22342 case still
recovers (list[bool] yields T = bool).
@xh123637

xh123637 commented Nov 6, 2026

Copy link
Copy Markdown
Author

Thanks for the primer run — it caught a real regression, and I have fixed it.

The pandas case (set_names, names = [names] # type: ignore[assignment]): after the context union simplified to Hashable | dict[...], no remaining item produced a constraint for list[T], yet the recovery still added one (T = Hashable from the dropped Sequence[Hashable]), turning the old assignment error into a list-item error the ignore does not cover.

I have narrowed the recovery so it only runs when at least one surviving union item already produced a constraint — recovery exists to offer an alternative to a (possibly wrong) commitment, and here there was no commitment to correct, so the literal fallback handles it as before. The #22342 case is unaffected (its list[bool] produces T = bool, so recovery still applies there).

Added a regression test for the pandas-shaped case (testListLiteralContextUnionWithUnhashableDictMember), verified it fails before the narrowing and passes after. Full suite (testcheck + testtypes + testsolve + testinfer) is green: 8469 passed, 0 failed.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False positive list-item: list literal checked against list[bool] when the union also contains Hashable

1 participant