I have a function, it’s return type is tuple[bool, set[int] | str]
. If the 0th item is True
, then the 1st item is the result set[int]
, else the 1st item is a str showing the reason why it failed.
It’s like this:
def callee(para_a: int) -> tuple[bool, set[int] | str]:
result = set([1, 2, 3])
if (something wrong):
return False, "something wrong"
return True, result
Now I call this function from other functions:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
api = FastAPI()
def kernel(para: set[int]):
return [i for i in para]
@api.post("/test")
def caller(para_a: int):
res = callee(para_a)
if res[0] is True:
return {"result": kernel(res[1])}
return JSONResponse(status_code=500, content={"fail_msg": res[1]}
Never mind fastapi
, that’s just because I want to say it is sometimes useful in web.
Now mypy
would blame that error: Argument 1 to "kernel" has incompatible type "set[int] | str"; expected "set[int]" [arg-type]
, so I’d like to make mypy
know that if the 0th item is True
, then the 1st item is the result set[int]
, else the 1st item is a str. I thought about overload
, so I wrote.
from typing import overload, Literal
@overload
def callee(para_a: int) -> tuple[Literal[True], set[int]]:
...
@overload
def callee(para_a: int) -> tuple[Literal[False], str]:
...
Then mypy
blames that Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader
.
What I’d like to know is whether there is a good way to solve my problem?
As I can’t use overload
in this situation, what should I use to make mypy
know res[1]
is just a set[int]
but never would be a str
if res[0] is True
?
5
Yes, @M.O.’s answer is correct. However, instead of returning 2 types, you can also return the type on “succes” and raise an exception on “fail”, to reduce type complexity. It’s a different style.
def callee(para_a: int) -> set[int]:
if good:
return set(1,2,3)
else:
raise CustomException()
and using it:
try:
result = callee(para_a)
return {"result": kernel(result)}
except CustomException:
return JSONResponse(status_code=500, content={"fail_msg": "Your error message"}
1
Using the type tuple[Literal[True], set[int]] | tuple[Literal[False], str]
should work with mypy
.
From a non-typing-wise aspect, all of these narrowing are unnecessary and un-Pythonic. As suggested by @chepner in the comments and @MichaelvandeWaeter in their answer, you are better off using exceptions; that way, the return type will always be set[int]
. However, here’s an explanation if you are interested.
@M.O.’s answer works because the union of tuple[Literal[True], set[int]]
and tuple[Literal[False], str]
is a discriminated union, also known as a tagged union: The two subtypes can be discriminated based on the type of the first elements (Literal[True]
and Literal[False]
).
res[0] is True
is thus recognized as a valid type guard, narrowing res
to tuple[Literal[True], set[int]]
:
(playgrounds: Mypy, Pyright)
res = callee()
if res[0] is True:
reveal_type(res) # tuple[Literal[True], set[int]]
else:
reveal_type(res) # tuple[Literal[False], str]
Note that this does not apply if you unpack the tuple beforehand, because in that case the types of the elements no longer depend on each other and will therefore be narrowed separately:
(playgrounds: Mypy, Pyright)
a, b = callee()
if a is True:
reveal_type((a, b)) # tuple[Literal[True], set[int] | str]
else:
reveal_type((a, b)) # tuple[Literal[False], set[int] | str]
Here’s the important part: set[int] | str
is already a discriminated union. There can be no type that is a subtype of both of these types, since str
inherits from Collection[str]
and set[int]
from Collection[int]
. The first element is not needed for narrowing at all:
(playgrounds: Mypy, Pyright)
def callee() -> set[int] | str: ...
res = callee()
if isinstance(res, set):
reveal_type(res) # set[int]
return
reveal_type(res) # str