You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
False positive list-item: list literal checked against list[bool] when the union also contains Hashable #22342
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.
fromcollections.abcimportHashable, MutableSequencedeff(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)
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 listdoes 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.
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
Activity
TYP: cite python/mypy#22342, move set/dict negatives into existing tests
TYP: disallow `set` and `dict` indexers in `__getitem__` and `__setit…
Avoid false positive for list literals in union contexts
hamarshehmhmd commentedon Nov 3, 2026
Root cause is union simplification in
mypy/constraints.pycombined with non-transitive subtyping._infer_constraints()simplifies the context union before inferringlist[T] <: list[bool] | Hashable | MutableSequence[str]. Since typeshed'sMutableSequencedoesn't set__hash__ = None,MutableSequence[str]is a proper subtype ofHashable, so it gets dropped and the union becomeslist[bool] | Hashable. Butlistdoes set__hash__ = None, solist[T]can't matchHashable(infer_constraints_if_possiblereturnsNone). That leaveslist[bool]as the only option, soT = bool, and the items get checked againstbool. That's also why swappingHashableforintmakes 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.With this,
[bool]and[str]become two competing options,any_constraintsgives up on the context, andTis inferred from the items, which is what pyright/ty do.Tested on master (623bef9):
f([True])still passes;g(["a"])withlist[bool] | Hashablestill errors withlist-itemf([1.5])still errors, but now asarg-typeagainst the whole union (with the__hash__conflict note) instead oflist-itemvsbool, which I think is more accurateClassVar[None]override (fixtures don't modellist.__hash__) and a pythoneval test with the exact repro; both fail before, pass aftertestcheck.py,testtypes.py,testsolve.py,testinfer.py: 8503 passed, 29 skipped, 7 xfailedmypy/constraints.pyis cleanHappy to open a PR if this approach looks right.
Fix subclassing member with type and list literal context false positive
Fix false positive list-item for list literal against union with Hash…
Only recover dropped union options when a commitment exists