mypy lambda optional check

Viewed 1383

I noticed that mypy forgets type information when going inside the scope of a lambda. Below is a small example to explain:

from typing import Optional, Callable

def wrapper(x: Callable[[], None]):
    x()

def foo(a: int):
    print(str(a))

a: Optional[int] = 0

if a is None:
    exit()

wrapper(lambda: foo(a))

Mypy complains about this snippet with error: Argument 1 to "foo" has incompatible type "None"; expected "int". However, we already checked that a is not None before. This is surprisingly strange because if we were to call foo(a) directly (without the wrapper), it does work. I'm using python 3.6.10 with mypy 0.740.

Is this intended behavior? And if so, how would you go about typing this correctly?

Thanks!

1 Answers

This is not due to lambda, but due to a being a closure. In short, the function is technically wrong even though the specific code cannot trigger the erroneous path.

As with similar issues of fixing closures in functions, forcing the name to be evaluated at definition provides valid typing:

wrapper(lambda a=a: foo(a))

Note that mypy cannot properly infer lambda types, so using lambda like this will still result in an error Cannot infer type of lambda.


Consider the completely equivalent named function:

def l() -> None:
    # reveal_type(a) -> Union[builtins.int, None]
    return foo(a)

Notice how a: Optional[int] is visible here, not the reduced a: int after the a is None guard. So the issue is not from lambda instead of def.

The source of the problem is that both l and the lambda close over all values of a. Even though a: None has been rejected before defining l/lambda, it is valid to reset a later on.

a: Optional[int]

if a is None: exit()

def l() -> None:
    # reveal_type(a) -> Union[builtins.int, None]
    return foo(a)

a = None
Related