basedpyright icon indicating copy to clipboard operation
basedpyright copied to clipboard

FR: Type narrowing of lists based on any(isinstance) checks

Open Kayzels opened this issue 5 months ago • 4 comments

Description

It would be awesome if it could narrow the type based on an all isinstance check, if possible. This does a check that all the variables are of a specific type, so the inner block should know that narrowed type.

Code sample in basedpyright playground

from typing import reveal_type

def process_list(values: list[str] | list[list[str]]) -> str:
    if all(isinstance(item, str) for item in values):
        reveal_type(values) # Should be narrowed to list[str]
        return "Should be str"
    if all(isinstance(item, list) and len(item) == 2 for item in values):
        reveal_type(values) # Should be narrowed to list[list[str]]
        return "Should be list[str]"
    return ""

Kayzels avatar Sep 18 '25 07:09 Kayzels

the issue is that there is no type system contract that all does what you expect it does. therefore requiring special-casing, which is not ideal

@DetachHead does builtins specialization count as a plugin?

also, did you consider this case:

def process_list(values: list[str] | list[int]):
    if all(isinstance(item, str) for item in values):
        values.append("i'm a string")
a: list[int] = []
process_list(a)
print(a)

i think for this particular case you might want to use any instead of all

also, did you consider this case:

class A: ...

def process_list(values: list[A] | list[list[str]]) -> str:
    if all(isinstance(item, list) for item in values):
        values.append(["a"])
        return "Should be list[str]"
    if all(isinstance(item, A) for item in values):
        return "Should be A"
    return ""


class ItsNotDisjoint(A, list[int]): ...


data: list[A] = [ItsNotDisjoint()]
process_list(data)

i think in an ideal world we could narrow any to some kind of list[Covariant[str]]

KotlinIsland avatar Sep 18 '25 13:09 KotlinIsland

does builtins specialization count as a plugin?

while i don't like special-casing, don't think it necessitates a plugin since there is already a lot of special casing for builtins

def process_list(values: list[str] | list[int]):
    if all(isinstance(item, str) for item in values):
        values.append("i'm a string")
a: list[int] = []
process_list(a)
print(a)

good point, narrowing a list this way isn't safe because there's no way to tell what the type is if the list is empty

DetachHead avatar Sep 20 '25 04:09 DetachHead

if the list is empty

or it's not disjoint

KotlinIsland avatar Sep 20 '25 04:09 KotlinIsland

i guess its safe to narrow any but not all

or it's not disjoint

separate issue with narrowing in general: #1444

DetachHead avatar Sep 20 '25 05:09 DetachHead