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