from typing import TypeGuard
class A:
def foo(self): ...
class B:
def bar(self): ...
def guard(it: object) -> TypeGuard[A]: ...
b = B()
if guard(b):
b.foo()
b.bar() # error: "A" has no attribute "bar"
if isinstance(b, A):
b.foo()
b.bar() # no error
When a conditional statement includes a call to a user-defined type guard function, and that function returns true, the expression passed as the first positional argument to the type guard function should be assumed by a static type checker to take on the type specified in the TypeGuard return type
@erictraut The pep for TypeGuard doesn't mention this scenario and pyright behaves the same as mypy.
Currently within the isinstance block, mypy will narrow the type to an intersection of both types <subclass of "B" and "A"> but within the TypeGuard block, mypy will instead change the type entirely to the guarded type.
Original description
Originally this issue was about the more specific scenario of a derived type being widened to the guarded type.
class A:
...
class B(A):
def foo(self): ...
def foo(it: object) -> TypeGuard[A]: ...
b = B()
if foo(b):
b.foo() # error: "A" has no attribute "foo"
When a conditional statement includes a call to a user-defined type guard function, and that function returns true, the expression passed as the first positional argument to the type guard function should be assumed by a static type checker to take on the type specified in the TypeGuard return type
@erictraut The pep for TypeGuard doesn't mention this scenario and pyright behaves the same as mypy.
In TypeScript the type guard will narrow the type to an intersection of the both types, but Python doesn't have Intersections (yet)
I think the pep should be updated such that if the type guarded type is a super type of the input then it shouldn't be 'narrowed' (actually it's widened) it should be left alone.
Workaround
Currently this only works in pyright.
from typing import TypeGuard, TypeVar
class A:
...
class B(A):
def foo(self): ...
T_foo = TypeVar("T_foo", bound=A)
def foo(it: T_foo | object) -> TypeGuard[T_foo]: ...
b = B()
if foo(b):
b.foo() # works!
@erictraut The pep for
TypeGuarddoesn't mention this scenario and pyright behaves the same as mypy.Currently within the
isinstanceblock, mypy will narrow the type to an intersection of both types<subclass of "B" and "A">but within theTypeGuardblock, mypy will instead change the type entirely to the guarded type.Original description
Originally this issue was about the more specific scenario of a derived type being widened to the guarded type.
@erictraut The pep for
TypeGuarddoesn't mention this scenario and pyright behaves the same as mypy.In TypeScript the type guard will narrow the type to an intersection of the both types, but Python doesn't have Intersections (yet)
I think the pep should be updated such that if the type guarded type is a super type of the input then it shouldn't be 'narrowed' (actually it's widened) it should be left alone.
Workaround
Currently this only works in pyright.