Skip to content

TypeVar bound to Union in if isinstance(...) branch has incompatible return type #12800

Closed as not planned

Description

@headtr1ck

Bug Report

A TypeVar bound to Union[x, y] will resolve to x after an if isinstance(..., x) is used which is incompatible with the return type.

I'm not sure on how to describe exactly what is going on, better check the Example.
Also mypy experts, please forgive me if that is intended behavior and not a bug (I would appreciate a working solution in this case :)

To Reproduce

Example:

from typing import TypeVar, Union

T = TypeVar("T", bound=Union[str, int])

def a(x: T) -> T:
    if isinstance(x, str):
        return x
    return x

Expected Behavior

I expect that this definition is correct, since whatever input is given, the output is the same.

Actual Behavior

mypy raises error: Incompatible return value type (got "str", expected "T").

Further Info

  • I want to have the str -> str, int -> int mapping, so simply using Union[str, int] won't work.
  • This function is called sometimes with variables of type Union[str, int], so using TypeVar("T", str, int) won't work either.

Your Environment

  • Mypy version used: 0.950
  • Mypy command-line flags: --strict
  • Mypy configuration options from mypy.ini (and other config files):
  • Python version used: 3.9
  • Operating system and version: win10

Activity

  1. headtr1ck commented on May 16, 2022

    @headtr1ck
    Author

    As a side note: a reveal_type(x) in the else branch returns "T`-1". Maybe a more expressive type would be useful?

  2. AlexWaygood commented on May 16, 2022

    @AlexWaygood
    Member

    This one looks like a bug to me :)

    For a (horrible) workaround, you can use overloads instead:

    from typing import overload
    
    @overload
    def a(x: str) -> str: ...
    
    @overload
    def a(x: int) -> int: ...
    
    @overload
    def a(x: int | str) -> int | str: ...
    
    def a(x: int | str) -> int | str:
        if isinstance(x, str):
            return x
        return x
    
    b: str
    c: int
    d: int | str
    
    reveal_type(a(b))  # Revealed type is "builtins.str"
    reveal_type(a(c))  # Revealed type is "builtins.int"
    reveal_type(a(d))  # Revealed type is "Union[builtins.int, builtins.str]"
  3. sterliakov commented on May 17, 2022

    @sterliakov
    Collaborator

    I don't know what does it mean for debugging, but this is also a bit odd:

    from typing import TypeVar, Union
    
    T = TypeVar("T", bound=Union[str, int])
    
    def a(x: T) -> T:
        reveal_type(x)  # T`-1
        if isinstance(x, str):
            reveal_type(x)  # str
            return x
        reveal_type(x)  # T`-1
        return x

    Gives T`-1, str and T`-1 revealed (in order) and error on line 9 (1st return). However, the following:

    from typing import TypeVar, Union
    
    T = TypeVar("T", bound=Union[str, int])
    
    def a(x: T) -> T:
        reveal_type(x)  # T`-1
        if isinstance(x, str | None):
            reveal_type(x)  # T`-1
            return x
        reveal_type(x)  # T`-1
        return x

    Gives T`-1 three times and no error. But I just add None which is not a member of T bounds, so it shouldn't affect inference, right?.. Say, the following ignores None in similar case:

    x: str
    if isinstance(x, str | None):
        reveal_type(x)  # str

    But... Maybe it should be a separate issue, given this weird result with mypy from master and python 3.10:

    x: str | int
    if isinstance(x, str | None):
        reveal_type(x)  # N: Revealed type is "Union[builtins.str, builtins.int]"
  4. AlexWaygood commented on May 17, 2022

    @AlexWaygood
    Member

    @headtr1ck, a slightly simpler and less horrible workaround: just use a cast():

    from typing import TypeVar, Union, cast
    
    T = TypeVar("T", bound=Union[str, int])
    
    def a(x: T) -> T:
        if isinstance(x, str):
            return cast(T, x)
        return x
  5. headtr1ck commented on May 17, 2022

    @headtr1ck
    Author

    @headtr1ck, a slightly simpler and less horrible workaround: just use a cast():

    This is what I ended up doing in the moment.

  6. gschaffner commented on Oct 13, 2022

    @gschaffner
    Contributor

    Is this a duplicate of #10817?

  7. hauntsaninja commented on Jan 10, 2023

    @hauntsaninja
    Collaborator

    Closing as duplicate of #10817

  8. moranabadie commented on Jun 25, 2024

    @moranabadie

    Closing as duplicate of #10817

    #10817 is closed but the problem persists in python 1.10.1

  9. AlexWaygood commented on Jun 25, 2024

    @AlexWaygood
    Member

    #10817 is closed

    not true, it is still open

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions