Files
ruff/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md
David Peter 2c57c2dc8a [red-knot] Type narrowing for isinstance checks (#13894)
## Summary

Add type narrowing for `isinstance(object, classinfo)` [1] checks:
```py
x = 1 if flag else "a"

if isinstance(x, int):
    reveal_type(x)  # revealed: Literal[1]
```

closes #13893

[1] https://docs.python.org/3/library/functions.html#isinstance

## Test Plan

New Markdown-based tests in `narrow/isinstance.md`.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-10-23 20:51:33 +02:00

3.3 KiB

Narrowing for isinstance checks

Narrowing for isinstance(object, classinfo) expressions.

classinfo is a single type

x = 1 if flag else "a"

if isinstance(x, int):
    reveal_type(x)  # revealed: Literal[1]

if isinstance(x, str):
    reveal_type(x)  # revealed: Literal["a"]
    if isinstance(x, int):
        reveal_type(x)  # revealed: Never

if isinstance(x, (int, object)):
    reveal_type(x)  # revealed: Literal[1] | Literal["a"]

classinfo is a tuple of types

Note: isinstance(x, (int, str)) should not be confused with isinstance(x, tuple[(int, str)]). The former is equivalent to isinstance(x, int | str):

x = 1 if flag else "a"

if isinstance(x, (int, str)):
    reveal_type(x)  # revealed: Literal[1] | Literal["a"]

if isinstance(x, (int, bytes)):
    reveal_type(x)  # revealed: Literal[1]

if isinstance(x, (bytes, str)):
    reveal_type(x)  # revealed: Literal["a"]

# No narrowing should occur if a larger type is also
# one of the possibilities:
if isinstance(x, (int, object)):
    reveal_type(x)  # revealed: Literal[1] | Literal["a"]

y = 1 if flag1 else "a" if flag2 else b"b"
if isinstance(y, (int, str)):
    reveal_type(y)  # revealed: Literal[1] | Literal["a"]

if isinstance(y, (int, bytes)):
    reveal_type(y)  # revealed: Literal[1] | Literal[b"b"]

if isinstance(y, (str, bytes)):
    reveal_type(y)  # revealed: Literal["a"] | Literal[b"b"]

classinfo is a nested tuple of types

x = 1 if flag else "a"

if isinstance(x, (bool, (bytes, int))):
    reveal_type(x)  # revealed: Literal[1]

Class types

class A: ...


class B: ...


def get_object() -> object: ...


x = get_object()

if isinstance(x, A):
    reveal_type(x)  # revealed: A
    if isinstance(x, B):
        reveal_type(x)  # revealed: A & B

No narrowing for instances of builtins.type

t = type("t", (), {})

# This isn't testing what we want it to test if we infer anything more precise here:
reveal_type(t)  # revealed: type
x = 1 if flag else "foo"

if isinstance(x, t):
    reveal_type(x)  # revealed: Literal[1] | Literal["foo"]

Do not use custom isinstance for narrowing

def isinstance(x, t):
    return True


x = 1 if flag else "a"
if isinstance(x, int):
    reveal_type(x)  # revealed: Literal[1] | Literal["a"]

Do support narrowing if isinstance is aliased

isinstance_alias = isinstance

x = 1 if flag else "a"
if isinstance_alias(x, int):
    reveal_type(x)  # revealed: Literal[1]

Do support narrowing if isinstance is imported

from builtins import isinstance as imported_isinstance

x = 1 if flag else "a"
if imported_isinstance(x, int):
    reveal_type(x)  # revealed: Literal[1]

Do not narrow if second argument is not a type

x = 1 if flag else "a"

# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "a"):
    reveal_type(x)  # revealed: Literal[1] | Literal["a"]

# TODO: this should cause us to emit a diagnostic during
# type checking
if isinstance(x, "int"):
    reveal_type(x)  # revealed: Literal[1] | Literal["a"]

Do not narrow if there are keyword arguments

x = 1 if flag else "a"

# TODO: this should cause us to emit a diagnostic
# (`isinstance` has no `foo` parameter)
if isinstance(x, int, foo="bar"):
    reveal_type(x)  # revealed: Literal[1] | Literal["a"]