Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type narrowing via exception in function

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:

  1. Are there scenarios where calling ensure_int would not guarantee that the type of x is int?
  2. Is there a way of fixing this with additional annotations or similar? I read about TypeGuard and TypeIs but they only work with functions which return bool.
like image 610
Stan Avatar asked Oct 20 '25 19:10

Stan


2 Answers

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.)

like image 130
kojiro Avatar answered Oct 22 '25 09:10

kojiro


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
like image 41
Valentin Avatar answered Oct 22 '25 11:10

Valentin



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!