Skip to content

Incorrect narrowing over overlapping union with isinstance #2169

@randolf-scholz

Description

@randolf-scholz

Describe the Bug

from collections.abc import Iterable, Mapping
from typing import reveal_type, assert_type

def test1(arg: Mapping[str, int] | Iterable[tuple[str, int]]) -> None:
    if isinstance(arg, Mapping):
        reveal_type(arg)  # E: Mapping[str, int] | Mapping[tuple[str, int], ?]
    else:
        reveal_type(arg)  # E: Iterable[tuple[str, int]]

def test2(arg: Mapping[str, int] | Iterable[tuple[str, int]]) -> None:
    if not isinstance(arg, Mapping):
        reveal_type(arg)  # E: Iterable[tuple[str, int]]
    else:
        reveal_type(arg)  # E: Mapping[str, int] | Mapping[tuple[str, int], ?]

Actual output:

INFO sandbox.py:6:20-25: revealed type: Mapping[tuple[str, int], Unknown] 
INFO sandbox.py:8:20-25: revealed type: Iterable[tuple[str, int]] 
INFO sandbox.py:12:20-25: revealed type: Iterable[tuple[str, int]] 
INFO sandbox.py:14:20-25: revealed type: Mapping[tuple[str, int], Unknown]

Explanation:

  • In the first test, narrowing by isinstance(arg, Mapping), gives us a union because Iterable[tuple[str, int]] & Mapping = Mapping[tuple[str, int], ?] is not empty. In the else branch, we can exclude Mapping so only Iterable[tuple[str, int]] remains
  • In the second test, narrowing by not isinstance(arg, Mapping), then in the if-branch the type should be Iterable - Mapping, and in the else branch we get (Mapping | Iterable) - (Iterable - Mapping) which simplifies to (Mapping | (Mapping & Iterable))
Note: formally, both tests are equivalent
def test1(arg: T) -> None:
    if isinstance(arg, X):
        # T & X
    else:
        # T - (T & X) = T - X

def test2(arg: T) -> None:
    if not isinstance(arg, X):
        # T - (T&X) = T - X
    else:
        # T - (T - (T&X)) = T & X

Sandbox Link

https://pyrefly.org/sandbox/?project=N4IgZglgNgpgziAXKOBDAdgEwEYHsAeAdAA4CeS4ATrgLYAEAxrlLAwC4S7pyGrYN0INYrkps6ASTYxKfWABo6AWVTFiEdAHMAOujDV6bUuq2Dho8ZRgA3GKigB9I8RiLUcODLZPjMXbswYMDppODYARgAKVEpNRGVVE00AbTDKRQ02AF06AB9JaVlsWGS2AFdiErSM9GysgEo6AFoAPjoAOS4YRF06PsFgiDgNMIwGGGjYxRU1DU16nvR%2B5borW3sfF0n5voBiOgBReJmk1LZ0wVqc-JO50oqq85rsxQB%2BLN7%2BmChPRZX%2BtZ2RzOCYxHZ0fZHAoyOQwe6VOHVS51fxYIIheBsABM22OiTuSMy12hRRK5QRZwuRIazTanXQ3U%2BfQgwXQuHEQxGbDGoKmCVmWgWTJWgI2IO2jQhh3iUhhxTh5MeVKuHyWXx%2BjLV-1FwN8Er20v5p0JVzyRruisRT2RWTeqpA8hAZQ4PxI5EQIH2AFUXRAjHQwGV0OxONxUYFgmBRDRUN50GUaNgZJF8PFMo1WnQ0n8ATBypQlmBtCB2gmk5R4sB8ABfYu6B0gMhWMBQUiENi0KAUfYABVIzdbWYwOAIjC4kE0ZVkHC4hF0%2BwAyjAYHQABZsNjEOCIAD0O6bQVbhFEmh3MHQO8wuAYcB3TD0EEn09DO4DojoqGsqGgsLHD6fsahnQuDEDO3BzugZBsKuXBNLYlDDFwdAALx0MWADMhDhFidboCA1aOqgIa2AAYtAMAUGgWB4EQZD4UAA

(Only applicable for extension issues) IDE Information

No response

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions