Mypy type narrowing from assertion - python

I have two variables a and b that are either int or str.
I write an assertion that insists a and b are either both ints or strs.
If I typenarrow a to an int, is there a way for mypy to infer b is also an int?
Here is some sample code.
Mypy version:
mypy 0.980+dev.0f17aff06ac1c05c442ba989e23655a2c6adbfbf (compiled: no)
Thanks for your help.
def my_func(a: int | str, b: int | str):
# We assert one of the statements is true: 1) a and b are ints, or 2) a and b are strings.
# In the case of an int a, and a string b, or vice versa, this assertion will fail.
assert isinstance(a, int) == isinstance(b, int)
# Another alternative assertion
assert type(a) == type(b)
if isinstance(a, int):
reveal_type(b) # mypy still thinks b is int or str

Using typing.TypeVar:
from typing import TypeVar
T = TypeVar('T', int, str)
def reveal_type(a: int):
pass
def foo(a: str):
pass
def my_func(a: T, b: T):
if isinstance(a, int):
reveal_type(b) # pass
else:
foo(b) # pass
If we simply exchange the calling positions of the two functions, mypy will find that they are all wrong calls and give two errors:
def my_func(a: T, b: T):
if isinstance(a, int):
foo(b) # Argument 1 to "foo" has incompatible type "int"; expected "str" (16:12)
else:
reveal_type(b) # Argument 1 to "reveal_type" has incompatible type "str"; expected "int" (18:20)

Related

typehint for tuple constructor

The following code gives error for tuple constructor:
def foo() -> tuple[int, int, bool]:
a: int = 1
b: int = 2
c: bool = True
results: tuple[int, int, bool] = tuple((a, b, c)) # mypy fails
results: tuple[int, int, bool] = (a, b, c) # mypy success
results: tuple[int, int, bool] = a, b, c # mypy success
return results
Error as below:
error: Incompatible types in assignment (expression has type "Tuple[object, ...]", variable has type "Tuple[int, int, bool]") [assignment]
edited:
error: Incompatible types in assignment (expression has type "Tuple[int, ...]", variable has type "Tuple[int, int, bool]")
Note, I get a slightly different error:
x.py:6: error: Incompatible types in assignment (expression has type "Tuple[int, ...]", variable has type "Tuple[int, int, bool]") [assignment]
Found 1 error in 1 file (checked 1 source file)
With:
mbp16-2019:~ jarrivillaga$ mypy --version
mypy 0.990 (compiled: yes)
So, you can think of tuple being type-hinted like this (see that actual typeshed hints here:
import typing
T = typing.TypeVar("T")
class tuple(object):
def __new__(cls, data: typing.Iterable[T]) -> tuple[T, ...]:
...
So the type of the literal:
data: tuple[int, int, bool] = (a, b, c)
gets promoted to:
data: tuple[int, ...]
Note, that bool is a subclass of int.
I suspect this is what is happening with your real example, (maybe you aren't using bool but something like str, in which case it has to promote to object).

Should mypy infer type from Union options?

In the following code fn_int will only be called with an int as an argument, yet mypy complains I might be passing it a str (viceversa for fn_str).
Is there something I'm missing here?
Should/can I somehow narrow down the type before passing arguments to fn_int and fn_str?
from typing import Union
def fn_int(arg: int) -> None:
print(arg)
def fn_str(arg: str) -> None:
print(arg)
def fn(arg: Union[str, int]) -> None:
if arg in range(4):
fn_int(arg)
elif arg == "str":
fn_str(arg)
else:
raise ValueError
> mypy mypy_test.py
mypy_test.py:15: error: Argument 1 to "fn_int" has incompatible type "Union[str, int]"; expected "int" [arg-type]
mypy_test.py:17: error: Argument 1 to "fn_str" has incompatible type "Union[str, int]"; expected "str" [arg-type]
What you're looking for is called type narrowing, and mypy doesn't do type narrowing for == or in comparisons. After all, they don't really guarantee type. As a fairly common example, after x == 3 or x in range(5), x could still be a float rather than an int. (Sure, a standard string won't pass an in range(4) check, and a standard int won't pass an == "str" check, but you could have a weird subclass.)
mypy will do type narrowing for the following kinds of expressions:
isinstance(x, SomeClass) narrows x to SomeClass.
issubclass(x, SomeClass) narrows x to Type[SomeClass].
type(x) is SomeClass narrows x to SomeClass.
callable(x) narrows x to callable type.
You can also write your own type guards with typing.TypeGuard, and mypy will understand those too.
arg is str or int, but you never check which type it is,
therefore check with if isinstance(arg, int).
then mypy knows that this function is only called if it is of desired type.
Example:
if isinstance(arg, int):
fn_int(arg)
elif isinstance(arg, str):
fn_str(arg)
else:
raise ValueError

Function with any number of int params typing hint

def f() -> Callable[[ # how to show there can be any number of int?
], float]:
def g(*args):
assert all(type(x) == int for x in args)
return 0.1
return g
I read the typing docs and Callable (i.e. Callable[…, ReturnType]) is not what I need.
I know Tuple[int, …], but Callable[[int, …], float] return Error "…" not allowed in this context Pylance.
You can do this by defining a Protocol with a __call__ whose function signature has the desired typing:
from typing import Protocol
class IntCallable(Protocol):
def __call__(self, *args: int) -> float: ...
def f() -> IntCallable:
def g(*args: int) -> float:
assert all(type(x) == int for x in args)
return 0.1
return g
Testing it out with mypy:
f()(1, 2, 3) # fine
f()("foo") # error: Argument 1 to "__call__" of "IntCallable" has incompatible type "str"; expected "int"
The other option is to have your function take a single Iterable[int] argument instead of an arbitrary number of int arguments, which lets you use a simple Callable typing instead of having to go the more complex Protocol route.

Use attribute from Optional[Union[str, int]] parameter depending on its type

I have a parameter of type: a: Optional[Union[str, int]].
I want to use some attributes when it is a string and other ones when it's an integer.
Eg:
if type(a) is int:
self.a = a
elif type(a) is str and a.endswith('some prefix'):
self.b = a
However, MyPy complains with the following:
error: Item "int" of "Union[str, int, None]" has no attribute "endswith"
error: Item "None" of "Union[str, int, None]" has no attribute "endswith"
Is there a way to make this work with MyPy?
The idiom you should be using is isinstance(a, int) instead of type(a) is int. If you do the former and write:
if isinstance(a, int):
self.a = a
elif isinstance(a, str) and a.endswith('some_prefix'):
self.b = a
...then your code should type-check cleanly.
The reason why doing type(a) is int isn't supported/most likely won't be supported any time soon is because what you're basically asserting there is that 'a' is exactly an int and no other type.
But we actually don't have a clean way of writing such a type in PEP 484 -- if you say some variable 'foo' is of type 'Bar', what you're really saying is that 'foo' could be of type 'Bar' or any subclass of 'Bar'.

type safety (mypy) for function parameters when using *args

Type-checking the following code with mypy:
def foo(a: str, b: float, c: int):
print(a, b, c + 1)
foo('ok', 2.2, 'bad')
reveals the invalid call too foo with:
error: Argument 3 to "foo" has incompatible type "str"; expected "int"
Now let's say we have a wrapper function like the following:
from typing import Callable, Any
def say_hi_and_call(func: Callable[..., Any], *args):
print('Hi.')
func(*args)
and do an invalid call using it
say_hi_and_call(foo, 'ok', 2.2, 'bad')
mypy will not report any errors, instead we will only get to know about this error at runtime:
TypeError: must be str, not int
I'd like to catch this error earlier. Is there a possibility to refine the type annotations in a way that mypy is able to report the problem?
OK, the only solution I came up with is making the arity of the function explicit, i.e.
from typing import Any, Callable, TypeVar
A = TypeVar('A')
B = TypeVar('B')
C = TypeVar('C')
def say_hi_and_call_ternary(func: Callable[[A, B, C], Any], a: A, b: B, c: C):
print('Hi.')
func(a, b, c)
def foo(a: str, b: float, c: int):
print(a, b, c + 1)
say_hi_and_call_ternary(foo, 'ok', 2.2, 'bad')
Of course one would need a similar say_hi_and_call_unary and say_hi_and_call_binary etc. too.
But since I value my application not exploding in PROD over saving some LOC, I'm happy when mypy is able to report the error, which now certainly is the case:
error: Argument 1 to "say_hi_and_call_ternary" has incompatible type "Callable[[str, float, int], Any]"; expected "Callable[[str, float, str], Any]"

Categories