pyright icon indicating copy to clipboard operation
pyright copied to clipboard

Adding a for loop breaks narrowing of a constrained TypeVar

Open camillol opened this issue 3 months ago • 1 comments

Suppose you have a function that can process a few different types (Foo, Bar), and return an object of the same type. But the result is only guaranteed to be a Foo if the input was a Foo and a Bar if it was a Bar; it's not guaranteed to be the specific Foo subclass you might have passed.

This can be represented using overloads, but it's verbose. A more concise approach is to use a TypeVar constrained to Foo, Bar. Then you can write this:

from typing import TypeVar

class Foo:
    def __call__(self, name: str, x: int) -> int:
        return 0

class FooDerived(Foo):
    def __call__(self, name: str, x: int) -> int:
        return 1

class Bar:
    def __call__(self, name: str, x: int) -> int:
        return 2

X = TypeVar("X", Foo, Bar)

def process(x: X) -> X:
    if isinstance(x, Bar):
        return Bar()
    else:
        return Foo()

reveal_type(process(Foo()))  # Foo
reveal_type(process(Bar()))  # Bar
reveal_type(process(FooDerived()))  # Foo

And it seems to work fine. But if you add a for loop, it forgets that X must be Foo in the else branch, and complains if you try to return a Foo:

def process_more_code(x: X) -> X:
    if isinstance(x, Bar):
        return Bar()
    else:
        for _ in range(1):
            pass
        return Foo()  # Type "Foo" is not assignable to return type "X@process_more_code"

Playground link

camillol avatar Sep 16 '25 00:09 camillol

I recommend against using value-constrained type variables. They are not well-specified in the Python typing spec, and it's not clear to me that it's possible to reconcile their behavior with other type system features. To my knowledge, not other programming language that supports generics supports this concept, and there's a good reason for it.

Value-constrained type variables are especially problematic for type checkers like pyright, which use lazy evaluation and are the basis for language servers. For more details about limitations of constrained type variables in pyright, refer to this documentation.

There's almost always a better way than using constrained type variables. In your code sample above, I recommend using overloads. Their behavior is well-specified in the typing spec, and you'll see consistent behavior across type checkers if you use them.

erictraut avatar Sep 16 '25 01:09 erictraut