Skip to content

Narrowing doesn't work on a typevar with a union bound #15631

Closed as not planned
Closed as not planned

Description

@eltoder

Bug Report

not isinstance check fails to narrow type for a TypeVar with a union bound.

To Reproduce

from typing import TypeVar

T = TypeVar("T", bound=int | str)

def foo(x: T) -> None:
    if not isinstance(x, int):
        reveal_type(x)

https://mypy-play.net/?mypy=latest&python=3.11&gist=2be94d37f0b12222d62a7f40e59fb152

Expected Behavior

Revealed type is "builtins.str".

Actual Behavior

Revealed type is "T`-1".

Your Environment

  • Mypy version used: 1.4.1
  • Python version used: 3.10.6

Activity

  1. A5rocks commented on Jul 18, 2023

    @A5rocks
    Collaborator

    By the way, the expected behavior is not correct:

    Imagine T is bound as some string subclass! If T were accepted as str then the following should probably be valid, correct?:

    from typing import TypeVar
    
    T = TypeVar("T", bound=int | str)
    
    def foo(x: T) -> T:
        if not isinstance(x, int):
            return "h"
        else:
            # whatever
            return x

    But that's not the case! The main problem here is typevar reveal_types cannot really express the narrowing as shown here. While mypy can do it under the hood (and you presumably have a case that shows this isn't the case?) that's not exactly what you're asking here!

    Mind clarifying a bit?

  2. eltoder commented on Jul 18, 2023

    @eltoder
    Author

    Good point, my example is not 100% correct. Let me clarify and also add a second problem case.

    1. A problem from your example that already happens with a positive isinstance check:
    from typing import TypeVar
    
    T = TypeVar("T", bound=int | str)
    
    def foo(x: T) -> T:
        if isinstance(x, int):
            reveal_type(x)
            return x
        else:
            return x

    This produces:

    typevar_narrow.py:7: note: Revealed type is "builtins.int"
    typevar_narrow.py:8: error: Incompatible return value type (got "int", expected "T")  [return-value]
    

    The type of x was narrowed to int and became incompatible with T even though the code is obviously correct. Here we did too much narrowing.

    1. A problem with not enough narrowing:
    from typing import TypeVar
    
    T = TypeVar("T", bound=int | str)
    
    def foo(x: T) -> None:
        if not isinstance(x, int):
            bar(x)
    
    def bar(x: str) -> None:
        pass

    This produces

    typevar_narrow.py:7: error: Argument 1 to "bar" has incompatible type "T"; expected "str"  [arg-type]
    

    but we know the type should be (a subclass of) str, because of the bound on T.

    To make it work in both cases, I think, rather than narrowing the type of x we should narrow the bound on T.

  3. brianschubert commented on Nov 14, 2024

    @brianschubert
    Collaborator

    Duplicate of #15235

  4. brianschubert commented on Nov 14, 2024

    @brianschubert
    Collaborator

    Actually, this issue is different from #15235. #15235 is only about the negative-narrowing behavior, whereas this issue is also about the positive-narrowing behavior. My bad, sorry for the noise!

  5. ilevkivskyi commented on May 31, 2025

    @ilevkivskyi
    Member

    The original example is not a bug in itself, the rest should be fixed by #19183

  6. jorgefandinno commented on Aug 3, 2025

    @jorgefandinno

    This seems not completely solved in mypy 1.17.1 when calling a method that return the type of the object.

    from typing import TypeVar, reveal_type
    
    class A:
        def f(self) -> 'A':
            return self
    
    class B:
        def f(self) -> 'B':
            return self
    
    T = TypeVar('T', bound=A | B)
    
    def g(arg: T) -> T:
        result = arg.f()
        reveal_type(result)
        return result

    produces

    narrow.py:15: note: Revealed type is "narrow.A | narrow.B"
    narrow.py:16: error: Incompatible return value type (got "A | B", expected "T")  [return-value]
    
  7. ilevkivskyi commented on Aug 3, 2025

    @ilevkivskyi
    Member

    @jorgefandinno This example is genuinely unsafe. I guess what you may want is something like this:

    from typing import Self, TypeVar, reveal_type
    
    class A:
        def f(self,) -> Self:
            return self
    
    class B:
        def f(self,) -> Self:
            return self
    
    T = TypeVar('T', bound=A | B)
    
    def g(arg: T) -> T:
        result = arg.f()
        reveal_type(result)
        return result

    This however doesn't work either, but it is caused by an unrelated bug. This is similar to what @sterliakov was working on at some point (but with def (Type[T]) -> T kind of things, see #14790), but looks like yet another problem with type variables with union bounds. I suggest opening a separate issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrong

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions