Skip to content

Define return type based on boolean flag #8634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
adithyabsk opened this issue Apr 5, 2020 · 9 comments
Closed

Define return type based on boolean flag #8634

adithyabsk opened this issue Apr 5, 2020 · 9 comments

Comments

@adithyabsk
Copy link

adithyabsk commented Apr 5, 2020

Situation

# scratch.py
from typing import Union

def test_func(ret_str = True) -> Union[int, str]:
    return "Hello " if ret_str else 0

if __name__ == "__main__":
    print(test_func(True)+"World!")
    print(test_func(False)+10)
$ mypy test.py

Error

scratch.py:7: error: Unsupported operand types for + ("int" and "str")
scratch.py:7: note: Left operand is of type "Union[int, str]"
scratch.py:8: error: Unsupported operand types for + ("str" and "int")
scratch.py:8: note: Left operand is of type "Union[int, str]"
Found 2 errors in 1 file (checked 1 source file)

Expectation

I, the programmer, know that I can expect a string from the functions as a result of the boolean flag. How would I go about clueing the static type checker into that as well. I know that I can use instanceof in the code that calls the function but that doesn't make much sense when one is guaranteed the return type due to the boolean flag.

@JelleZijlstra
Copy link
Member

You should use overloads that specify a different return type based on the value of the argument (either Literal[True] or Literal[False]).

Something like this (untested):

from typing import overload, Literal

@overload
def test_func(ret_str: Literal[True] = ...) -> str: ...
@overload
def test_func(ret_str: Literal[False]) -> int: ...

def test_func(ret_str: bool = True) -> Union[int, str]:
    return "Hello " if ret_str else 0

@adithyabsk
Copy link
Author

@JelleZijlstra Thanks for the help, that solved that particular issue!

@adithyabsk adithyabsk reopened this Apr 5, 2020
@adithyabsk
Copy link
Author

adithyabsk commented Apr 5, 2020

Actually, @JelleZijlstra followup issue, I might have spoken too soon. Is there any reason the second overload treats the ret_str as a positional argument?

Let's look at a slightly more complicated example.

# scratch.py
from typing import overload, Union
from typing_extensions import Literal

@overload
def test_func(arg1: int, kwarg1: int = 0, ret_str: Literal[True] = ...) -> str: ...
@overload
def test_func(arg1: int, kwarg1: int = 0, ret_str: Literal[False] = False) -> int: ...

def test_func(arg1: int, kwarg1: int = 0, ret_str: bool = True) -> Union[int, str]:
    return "Hello " if ret_str else 0


if __name__ == "__main__":
    print(test_func(0, 0, True)+"World!")
    print(test_func(0, 0, False)+10)

This outputs the error:

scratch.py:5: error: Overloaded function signatures 1 and 2 overlap with incompatible return types
scratch.py:15: error: Unsupported operand types for + ("str" and "int")
Found 2 errors in 1 file (checked 1 source file)

@JelleZijlstra
Copy link
Member

You should have a default for only one of the overloads, so they don't overlap.

@adithyabsk
Copy link
Author

@JelleZijlstra Sure that makes sense but maybe I don't completely follow. When I place the kwarg in front of the other kwarg, things certainly work.

from typing import overload, Union
from typing_extensions import Literal

@overload
def test_func(arg1: int, ret_str: Literal[True] = ..., kwarg1 = True) -> str: ...
@overload
def test_func(arg1: int, ret_str: Literal[False], kwarg1 = True) -> int: ...

def test_func(arg1: int, ret_str: bool = True, kwarg1: bool = True) -> Union[int, str]:
    return "Hello " if ret_str else 0


if __name__ == "__main__":
    print(test_func(0, True)+"World!")
    print(test_func(0, False)+10)

How would I go about making it work without having to change the position of the final kwarg?

@adithyabsk
Copy link
Author

I tried marking ret_str as a keyword-only argument as noted in #5486 but I'm getting the following error:

from typing import overload, Union
from typing_extensions import Literal

@overload
def test_func(arg1: int, kwarg1: int = 0, ret_str: Literal[True] = ...) -> str: ...
@overload
def test_func(arg1: int, kwarg1: int = 0, *, ret_str: Literal[False]) -> int: ...

def test_func(arg1: int, kwarg1: int = 0, ret_str: bool = True) -> Union[int, str]:
    return "Hello " if ret_str else 0


if __name__ == "__main__":
    print(test_func(0, 0, True)+"World!")
    print(test_func(0, 0, False)+10)

Error:

scratch.py:15: error: No overload variant of "test_func" matches argument types "int", "int", "bool"
scratch.py:15: note: Possible overload variant:
scratch.py:15: note:     def test_func(arg1: int, kwarg1: int = ..., ret_str: Literal[True] = ...) -> str
scratch.py:15: note:     <1 more non-matching overload not shown>
Found 1 error in 1 file (checked 1 source file)

@adithyabsk
Copy link
Author

Okay, after more debugging this works, which makes sense. I guess there isn't any way to make this work without converting ret_str to a keyword-only argument.

from typing import overload, Union
from typing_extensions import Literal

@overload
def test_func(arg1: int, kwarg1: int = 0, *, ret_str: Literal[True] = ...) -> str: ...

@overload
def test_func(arg1: int, kwarg1: int = 0, *, ret_str: Literal[False]) -> int: ...


def test_func(arg1: int, kwarg1: int = 0, *, ret_str: bool = True) -> Union[int, str]:
    return "Hello " if ret_str else 0


if __name__ == "__main__":
    print(test_func(0, 0, True)+"World!")
    print(test_func(0, 0, False)+10)

@knbk
Copy link
Contributor

knbk commented Apr 5, 2020

The following works:

from typing import overload, Union
from typing_extensions import Literal


@overload
def test_func(arg1: int, kwarg1: int = 0, ret_str: Literal[True] = ...) -> str: ...

@overload
def test_func(arg1: int, kwarg1: int, ret_str: Literal[False]) -> int: ...

@overload
def test_func(arg1: int, kwarg1: int = 0, *, ret_str: Literal[False]) -> int: ...


def test_func(arg1: int, kwarg1: int = 0, ret_str: bool = True) -> Union[int, str]:
    return "Hello " if ret_str else 0


if __name__ == "__main__":
    print(test_func(0) + "World!")
    print(test_func(0, 0) + "World!")
    print(test_func(0, 0, True) + "World!")
    print(test_func(0, 0, False) + 10)
    print(test_func(0, ret_str=False) + 1)
    print(test_func(0, 0, ret_str=False) + 1)
    print(test_func(0, ret_str=True) + "World!")
    print(test_func(0, 0, ret_str=True) + "World!")

When you pass False for ret_str, it's either a keyword argument and matches the keyword-only overload, or it's positional, in which case it must have an explicit value for kwarg1 and matches the overload without any defaults.

@adithyabsk
Copy link
Author

@knbk that's helpful, and makes sense: thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants