Skip to content

TypeGuard should change the type to an intersection like it does with isinstance #11229

@KotlinIsland

Description

@KotlinIsland
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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-typeguard-typeisTypeGuard / TypeIs / PEP 647 / PEP 742

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions