0

In the following code, the valid function and the invalid function do exactly the same thing. Why is MyPy happy with valid, but throws an error on invalid?

Isn't the TypeGuard suppose to handle that?

If I add a function to B and C only, and call that function from within a block guarded by ifBorC, that works fine.

Does MyPy not look at type guards when dealing with completeness of union types?

from typing import TypeGuard

class A: ...
class B: ...
class C: ...

ABC = A | B | C
BorC = B | C

def isBorC (x: ABC) -> TypeGuard[BorC]:
    return isinstance(x, B) or isinstance(x, C)

def valid (x: ABC) -> str:
    if isinstance(x, A):
        return 'a'
    if isinstance(x, B) or isinstance(x, C):
        return 'b or c'

def invalid (x: ABC) -> str:
    if isinstance(x, A):
        return 'a'
    if isBorC(x):
        return 'b or c'
    # Yields error: Missing return statement  [return]
0

1 Answer 1

1

This is a sharp corner of TypeGuard:

[TypeGuard] apply narrowing only in the positive case (the if clause). The type is not narrowed in the negative case.

TypeGuard § Type narrowing | Python typing spec

PEP 742 was created to solve this:

TypeIs and TypeGuard differ in the following ways:

  • [...]
  • When a TypeGuard function returns False, type checkers cannot narrow the type of the variable at all. When a TypeIs function returns False, type checkers can narrow the type of the variable to exclude the TypeIs type.

As a snippet:

(playgrounds: Mypy, Pyright)

def type_is(x: object, /) -> TypeIs[BorC]: ...
def type_guard(x: object, /) -> TypeGuard[BorC]: ...
def narrowing(x: ABC) -> None:
    reveal_type(x)      # Original: A | B | C

    if type_guard(x):
        reveal_type(x)  # Narrowed: B | C
        return
    
    reveal_type(x)      # Not narrowed: A | B | C

    if type_is(x):
        reveal_type(x)  # Narrowed: B | C
        return
    
    reveal_type(x)      # Also narrowed: A

Not the answer you're looking for? Browse other questions tagged or ask your own question.