Skip to content

Narrow negative TypeIs branches through Any in variant positions - #22041

Open
Dextheking1 wants to merge 1 commit into
python:masterfrom
Dextheking1:fix/typeis-narrow-any-variant
Open

Dextheking1 wants to merge 1 commit into
python:masterfrom
Dextheking1:fix/typeis-narrow-any-variant

Conversation

@Dextheking1

Copy link
Copy Markdown

Summary

Fixes #21641.

A negative TypeIs check failed to narrow a generic with Any in a
covariant/contravariant position:

T_co = TypeVar("T_co", covariant=True)

class Source(Generic[T_co]): ...

def is_source(x: object) -> TypeIs[Source[object]]: ...

def f(x: int | Source[Any]) -> None:
    assert not is_source(x)
    reveal_type(x)  # was: int | Source[Any]; now: int

Root cause

restrict_subtype_away() in mypy/subtypes.py uses strict proper subtype
checks, under which Any is only compatible with itself. So
Source[Any] was not recognised as covered by Source[object] purely because
of the Any argument, even though Any is compatible with every type in a
variant position. (The contravariant case already worked because its check goes
through a different path.)

Fix

  • Added an allow_any_in_variant_positions option to is_proper_subtype() /
    SubtypeContext: covariant/contravariant type arguments fall back to ordinary
    (Any-compatible) subtype checks, while invariant positions still require exact
    matches. The flag is part of the subtype cache key, so cached results stay
    correct.
  • restrict_subtype_away() now retries with this option before giving up, so
    C[Any] is correctly treated as covered by C[object] for variant C.
    Invariant generics (e.g. Box[Any] vs Box[str]) are still not narrowed.

Testing

  • New regression test testTypeIsNegativeNarrowCovariantGenericAny in
    test-data/unit/check-typeis.test: fails before, passes after. It covers
    covariant (narrowed), contravariant (still narrowed) and invariant (still kept).
  • mypy/test/testsubtypes.py: 26 passed.
  • check-typeis, check-narrowing, check-isinstance, check-generics,
    check-protocols, check-inference, check-bound, check-typevar-values,
    check-typeguard: 1269 passed, 6 skipped, 2 xfailed.
  • black --check clean; mypy self-check clean.

Note: this is a first-time contributor PR, so some workflow runs may show as
action_required pending maintainer approval.

Add allow_any_in_variant_positions to is_proper_subtype() so that
restrict_subtype_away() recognizes C[Any] as covered by C[object] for
covariant/contravariant C. Invariant positions still require exact matches.
Fixes python#21641.
@github-actions

Copy link
Copy Markdown
Contributor

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

@Dextheking1
Dextheking1 marked this pull request as ready for review September 24, 2026 20:27
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.

Missing TypeIs narrowing with covariant generic

1 participant