GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

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 #22342

New issue
Copy link
New issue
Copy link
Open
Open
False positive list-item: list literal checked against list[bool] when the union also contains Hashable#22342
Copy link
Labels
bugmypy got something wrongmypy got something wrong

Description

@cmp0xff
cmp0xff
opened on Nov 1, 2026
Issue body actions

Bug 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

Activity

  1. cmp0xff
    added
    bugmypy got something wrongmypy got something wrong
    on Nov 1, 2026
  2. cmp0xff
    added a commit that references this issue on Nov 1, 2026

    TYP: cite python/mypy#22342, move set/dict negatives into existing tests

    87d47d1
  3. loicdiridollou
    added a commit that references this issue on Nov 1, 2026

    TYP: disallow `set` and `dict` indexers in `__getitem__` and `__setit…

    93603c2
  4. Rohitpro1
    added a commit that references this issue on Nov 2, 2026

    Avoid false positive for list literals in union contexts

    This issue will close once commit a5c1d90 is merged into the 'master' branch.
    a5c1d90
  5. hamarshehmhmd commented on Nov 3, 2026

    @hamarshehmhmd
    hamarshehmhmd
    on Nov 3, 2026
    More actions

    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.

  6. Sudhakar7J
    added a commit that references this issue on Nov 4, 2026

    Fix subclassing member with type and list literal context false positive

    This issue will close once commit 8a02ccb is merged into the 'master' branch.
    8a02ccb
  7. xh123637
    added 2 commits that reference this issue on Nov 6, 2026

    Fix false positive list-item for list literal against union with Hash…

    This issue will close once commit e00c840 is merged into the 'master' branch.
    e00c840

    Only recover dropped union options when a commitment exists

    This issue will close once commit 02f793e is merged into the 'master' branch.
    02f793e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongmypy got something wrong

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.