I'm trying to understand why an exception raised based on the type of a variable doesn't narrow down the type of this variable.
I'd like to do something like this:
def ensure_int(obj: int | str) -> None:
if isinstance(obj, str):
raise ValueError("obj cannot be str")
def f(x: int | str) -> int:
ensure_int(x)
return x
I would've thought that calling ensure_int
in f
would narrow the type of x
down to int
, but it doesn't. Mypy gives:
error: Incompatible return value type (got "Union[int, str]", expected "int") [return-value]
Why does this not work? If I inline the code of ensure_int
into f
then the error goes away.
So, my questions are:
ensure_int
would not guarantee that the type of x
is int
?TypeGuard
and TypeIs
but they only work with functions which return bool
.Type checkers won't propagate type narrowing outside a function the way you're expecting, but you can create a user defined type guard, which is just a special case of a boolean function.
from typing import TypeGuard
def ensure_int(obj: int | str) -> TypeGuard[int]:
return isinstance(obj, int)
def f(x: int | str) -> int:
if ensure_int(x):
return x
raise ValueError("obj cannot be str")
(This example is trivial, because it's just a wrapper around isinstance, but I'm assuming there's a more sophisticated real-world use case involved for some users.)
The answer above with using TypeGuard
is probably the best way to go. Though as an alternative, you can also make ensure_int
return the object itself, but with the type narrowed:
def ensure_int(obj: int | str) -> int:
if isinstance(obj, str):
raise ValueError("obj cannot be str")
return obj
def f(x: int | str) -> int:
x = ensure_int(x)
return x
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With