GeezerVison is showing github.com — False positive `list-item`: `list` literal checked against `list[bool]` when the union also contains `Hashable` · Issue #22342 · python/mypy
False positive list-item: list literal checked against list[bool] when the union also contains Hashable #22342DescriptionBug Report
When a parameter is a union of list[bool], Hashable and MutableSequence[str], mypy reports ["a", "b"] as an invalid list[bool]. The MutableSequence[str] member accepts it. Dropping Hashable, or replacing it with int, makes the error go away. Pyright, pyrefly and ty accept the call. This looks related to #3506.
To Reproduce
https://mypy-play.net/?gist=035e1c2653c71379a5231a139f9f2fa2
from collections.abc import Hashable, MutableSequence
def f(k: list[bool] | Hashable | MutableSequence[str]) -> None: ...
f(["a", "b"]) # mypy: List item 1 has incompatible type "str"; expected "bool" [list-item]
What would work on mypy 2.4.1:
list[bool] | MutableSequence[str]
list[bool] | int | MutableSequence[str]
x = ["a","b"]; f(x).
Expected Behaviour
passing
Actual Behavior
main.py:5: error: List item 0 has incompatible type "str"; expected "bool" [list-item]
main.py:5: error: List item 1 has incompatible type "str"; expected "bool" [list-item]
Found 2 errors in 1 file (checked 1 source file)
Your Environment
- Mypy version used: 2.4.1 / 934e703
- Mypy command-line flags: none
- Mypy configuration options from
mypy.ini (and other config files): none
- Python version used: 3.15
Activityadded a commit that references this issue on Nov 2, 2026This issue will close once commit a5c1d90 is merged into the 'master' branch.a5c1d90 hamarshehmhmd commented on Nov 3, 2026 Root cause is union simplification in mypy/constraints.py combined with non-transitive subtyping.
_infer_constraints() simplifies the context union before inferring list[T] <: list[bool] | Hashable | MutableSequence[str]. Since typeshed's MutableSequence doesn't set __hash__ = None, MutableSequence[str] is a proper subtype of Hashable, so it gets dropped and the union becomes list[bool] | Hashable. But list does set __hash__ = None, so list[T] can't match Hashable (infer_constraints_if_possible returns None). That leaves list[bool] as the only option, so T = bool, and the items get checked against bool. That's also why swapping Hashable for int makes it go away: nothing gets absorbed.
Fix I tested: keep the unsimplified items around, and in the SUBTYPE_OF + union-context branch, add back the option for a dropped item X only when the template can match X but every item that absorbed X can't. That's exactly the "subtyping wasn't transitive here" case, so normal simplification behavior (e.g. list[int] | Sequence[int]) stays the same.
--- a/mypy/constraints.py
+++ b/mypy/constraints.py
@@ def _infer_constraints(
if not type_state.keep_unions and isinstance(template, UnionType):
template = mypy.typeops.make_simplified_union(template.items, keep_erased=True)
+ unsimplified_actual_items: list[Type] | None = None
if not type_state.keep_unions and isinstance(actual, UnionType):
+ unsimplified_actual_items = actual.items
actual = mypy.typeops.make_simplified_union(actual.items, keep_erased=True)
@@
if direction == SUBTYPE_OF and isinstance(actual, UnionType):
- return any_constraints(
- [
- infer_constraints_if_possible(template, a_item, direction)
- for a_item in actual.items
- ],
- eager=True,
- )
+ options = [
+ infer_constraints_if_possible(template, a_item, direction) for a_item in actual.items
+ ]
+ if unsimplified_actual_items is not None:
+ options.extend(
+ _options_lost_by_simplification(
+ template, actual.items, options, unsimplified_actual_items
+ )
+ )
+ return any_constraints(options, eager=True)
@@
+def _options_lost_by_simplification(
+ template: ProperType,
+ simplified_items: Sequence[Type],
+ simplified_options: list[list[Constraint] | None],
+ original_items: Sequence[Type],
+) -> list[list[Constraint] | None]:
+ """Constraint options for union items dropped by simplification that we still need.
+
+ Simplification drops X if it's a proper subtype of another item Y, but subtyping
+ isn't always transitive (MutableSequence[str] <: Hashable, list[str] isn't Hashable).
+ If the template can match X but none of the items that absorbed X can, keep X.
+ """
+ simplified_ids = {id(get_proper_type(t)) for t in simplified_items}
+ extra: list[list[Constraint] | None] = []
+ for item in original_items:
+ p_item = get_proper_type(item)
+ if id(p_item) in simplified_ids or isinstance(p_item, UnionType):
+ continue
+ absorbers = [
+ option
+ for y, option in zip(simplified_items, simplified_options)
+ if mypy.subtypes.is_proper_subtype(item, y, keep_erased_types=True)
+ ]
+ if not absorbers or any(option is not None for option in absorbers):
+ continue
+ option = infer_constraints_if_possible(template, item, SUBTYPE_OF)
+ if option is not None:
+ extra.append(option)
+ return extra
With this, [bool] and [str] become two competing options, any_constraints gives up on the context, and T is inferred from the items, which is what pyright/ty do.
Tested on master (623bef9):
- your repro passes;
f([True]) still passes; g(["a"]) with list[bool] | Hashable still errors with list-item
f([1.5]) still errors, but now as arg-type against the whole union (with the __hash__ conflict note) instead of list-item vs bool, which I think is more accurate
- added a check test with a custom protocol/
ClassVar[None] override (fixtures don't model list.__hash__) and a pythoneval test with the exact repro; both fail before, pass after
testcheck.py, testtypes.py, testsolve.py, testinfer.py: 8503 passed, 29 skipped, 7 xfailed
- self-check on
mypy/constraints.py is clean
Happy to open a PR if this approach looks right. added a commit that references this issue on Nov 4, 2026This issue will close once commit 8a02ccb is merged into the 'master' branch.8a02ccb added 2 commits that reference this issue on Nov 6, 2026This issue will close once commit e00c840 is merged into the 'master' branch.e00c840 This issue will close once commit 02f793e is merged into the 'master' branch.02f793e
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|