Skip to content

Keep ad-hoc intersections when narrowing with a tuple of types - #22141

Open
mohitduhan19 wants to merge 2 commits into
python:masterfrom
mohitduhan19:fix-isinstance-tuple-intersection
Open

mohitduhan19 wants to merge 2 commits into
python:masterfrom
mohitduhan19:fix-isinstance-tuple-intersection

Conversation

@mohitduhan19

Copy link
Copy Markdown

Fixes #22122

Problem

class A: ...
class B(A): ...
class C: ...

def f(x: A) -> None:
    if isinstance(x, (B, C)):
        reveal_type(x)  # before: "B"

B overlaps with A, so conditional_types() returns B | C. The ad-hoc intersection fallback in conditional_types_with_intersection() only runs when the whole result is uninhabited, so it never runs here. When the result is later narrowed against the declared type A, the C part is dropped and x becomes just B. A common subclass of A and C (e.g. class D(A, C)) still passes the check at runtime, so the narrowing is unsound, and mypyc-compiled code raises TypeError: B object expected; got D.

A single isinstance(x, C) already narrows correctly to <subclass of "A" and "C">.

Fix

When there are several type ranges and the overall result is inhabited, the yes-type is now built per range. A range that overlaps with the current type keeps its existing result. A range that is disjoint from it (an Instance on both sides) adds the same ad-hoc intersection that a single isinstance check would produce.

If no range is disjoint, the result is unchanged, so single-type checks and fully overlapping tuples behave exactly as before. isinstance(x, (B, C)) now narrows to B | <subclass of "A" and "C">.

Tests

Added testIsInstanceAdHocIntersectionTuplePartialOverlap (the issue's example, reversed tuple order, a fully overlapping tuple that stays unchanged, and a union declared type) and testIsInstanceAdHocIntersectionTuplePartialOverlapFinal (a @final disjoint class adds nothing).

Updated testIsInstanceWithStarExpressionAndVariable: with the test fixtures, int and tuple can have a common subclass, so the result now includes <subclass of "builtins.int" and "tuple[Any, ...]">. This matches what isinstance(var, tuple) alone already gives with the same fixtures. With real typeshed, int and tuple can't share a subclass, so users won't see this.

Locally, testcheck, testfinegrained, testdeps, testpythoneval and mypyc test_irbuild pass, and the self-check, black and ruff are clean.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

spark (https://github.com/apache/spark)
+ python/pyspark/sql/session.py:1236: error: Argument "names" to "_inferSchema" of "SparkSession" has incompatible type "<subclass of "pyspark.sql.types.DataType" and "tuple[Any, ...]"> | list[str] | None"; expected "list[str] | None"  [arg-type]
+ python/pyspark/sql/session.py:1278: error: Argument "names" to "_inferSchemaFromList" of "SparkSession" has incompatible type "<subclass of "pyspark.sql.types.DataType" and "tuple[Any, ...]"> | list[str] | None"; expected "list[str] | None"  [arg-type]

meson (https://github.com/mesonbuild/meson)
+ mesonbuild/optinterpreter.py:127:24: error: Incompatible return value type (got "int | float", expected "TYPE_elementary | HoldableObject | MesonInterpreterObject | Sequence[TYPE_var] | dict[str, TYPE_var]")  [return-value]
+ mesonbuild/ast/interpreter.py:692:24: error: Incompatible return value type (got "int | float", expected "str | int | bool | HoldableObject | MesonInterpreterObject | <6 more items>")  [return-value]

apprise (https://github.com/caronc/apprise)
- apprise/apprise_attachment.py:186: error: Item "AttachBase" of "str | AttachBase | list[str | AttachBase | AppriseAttachment]" has no attribute "__iter__" (not iterable)  [union-attr]
+ apprise/apprise_attachment.py:186: error: Item "AttachBase" of "str | AttachBase | <subclass of "tuple[Any, ...]" and "apprise.apprise_attachment.AppriseAttachment"> | list[str | AttachBase | AppriseAttachment]" has no attribute "__iter__" (not iterable)  [union-attr]

scipy-stubs (https://github.com/scipy/scipy-stubs)
- tests/misc/test_submodules_empty.pyi:3: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports

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.

isinstance with a tuple of types narrows unsoundly, causing TypeError in mypyc-compiled code

1 participant