improved type narrowing
when narrowing a type using an isinstance
check, there's no way for the type checker to narrow its type variables, so pyright just narrows them to "Unknown"):
def foo(value: object):
if isinstance(value, list):
reveal_type(value) # list[Unknown]
this makes sense in cases where the generic is invariant and there's no other way to represent any of its possibilities. for example if it were to be narrowed to list[object]
, you wouldn't be able to assign list[int]
to it. however in cases where the generic is covariant, contravariant, or uses constraints, it can be narrowed more accurately.
basedpyright introduces the new strictGenericNarrowing
setting to address this. the following sections explain how this new behavior effects different types of generics.
narrowing of covariant generics
when a type variable is covariant, its widest possible type is its bound, which defaults to object
.
when strictGenericNarrowing
is enabled, if a generic is covariant and does not have a bound, it gets narrowed to object
instead of "Unknown":
T_co = TypeVar("T_co", covariant=True)
class Foo(Generic[T_co]):
...
def foo(value: object):
if isinstance(value, Foo):
reveal_type(value) # Foo[object]
if the generic does have a bound, it gets narrowed to that bound instead:
T_co = TypeVar("T_co", bound=int | str, covariant=True)
class Foo(Generic[T_co]):
...
def foo(value: object):
if isinstance(value, Foo):
reveal_type(value) # Foo[int | str]
narrowing of contravariant generics
when a type variable is contravariant its widest possible type is Never
, so when strictGenericNarrowing
is enabled, contravariant generics get narrowed to Never
instead of "Unknown":
T_contra = TypeVar("T_contra", bound=int | str, covariant=True)
class Foo(Generic[T_contra]):
...
def foo(value: object):
if isinstance(value, Foo):
reveal_type(value) # Foo[Never]
narrowing of constraints
when a type variable uses constraints, the rules of variance do not apply - see this issue for more information. instead, a constraint declares that the generic must be resolved to be exactly one of the types specified.
when strictGenericNarrowing
is enabled, constrained generics are narrowed to a union of all possibilities:
class Foo[T: (int, str)]:
...
def foo(value: object):
if isinstance(value, Foo):
reveal_type(value) # Foo[int] | Foo[str]
this also works when there's more than one constrained type variable - it creates a union of all possible combinations:
class Foo[T: (int, str), U: (float, bytes)]:
...
def foo(value: object):
if isinstance(value, Foo):
reveal_type(value) # Foo[int, float] | Foo[int, bytes] | Foo[str, float] | Foo[str, bytes]