Python Type Hints - Lambdas don’t support type hints, but that’s okay

Python has no syntax to add type hints to lambda
s, but that doesn’t mean you can’t use them in type-checked code. In this post we’ll look at how Mypy can infer the types for lambda
s, based on where they’re used.
lambda
’s don’t support type hints
Take this simple lambda:
double = lambda x: x * 2
double(4501)
It’s intended to take an int
and return an int
. But check its type with reveal_type()
:
double = lambda x: x * 2
reveal_type(double)
…and Mypy tells says it takes and returns Any
:
$ mypy example.py
example.py:2: note: Revealed type is "def (x: Any) -> Any"
Success: no issues found in 1 source file
You might wonder if you can add type hints to a lambda
? Perhaps like:
double = lambda (x: int) -> None: x * 2 # doesn't work
Unfortunately not. There is no supported syntax, so the above is a SyntaxError
.
PEP 3107, “Function Annotations”, declared that lambda
would not support annotations (type hint syntax). The PEP gives three reasons:
- It would be an incompatible change.
- Lambdas are neutered anyway.
- The lambda can always be changed to a function.
(Lambdas are “neutered” in the sense that they only support a subset of function features, e.g. no keyword-only arguments.)
So, there’s no way to add type hints to a lambda
. But that’s normally fine…
Mypy infers types for lambda
’s
In most places you’d use a lambda
, Mypy can infer the argument type, type check the lambda, and infer the return type. It’s pretty neat!
Take this code:
numbers = [1, 2, 3]
doubled = map(lambda x: x * 2, numbers)
reveal_type(doubled)
Running Mypy, you can see the revealed type of doubled
:
$ mypy example.py
example.py:3: note: Revealed type is "builtins.map[builtins.int]"
Success: no issues found in 1 source file
Mypy sees that map()
will need a function that takes an int
, and uses that for the x
argument. Following the expression in the lambda
, Mypy can infer its return type is also int
. It then follows that doubled
will be a map[int]
(map()
returns the its own lazy iterable type).
If Mypy didn’t do this inference, it would only be able to declare doubled
as map[Any]
. This would be sad, since further usage would not be type-checked.
Mypy uses the inferred lambda
argument types to detect errors in the lambda
’s expression. For example, imagine you wanted to use int.bit_length()
on each number, but typo’d the name (no _
):
numbers = [1, 2, 3]
bit_lengths = map(lambda x: x.bitlength(), numbers)
Mypy could spot this:
$ mypy example.py
example.py:2: error: "int" has no attribute "bitlength"; maybe "bit_length"?
Found 1 error in 1 file (checked 1 source file)
Slick.
Inference only works on the same line
The above inference doesn’t work if the lambda
declaration is separate from its usage. For example, if you change the above example to store the lambda
in a variable:
numbers = [1, 2, 3]
double = lambda x: x * 2
doubled = map(double, numbers)
reveal_type(doubled)
…then doubled
has type map[Any]
:
$ mypy example.py
example.py:4: note: Revealed type is "builtins.map[Any]"
Success: no issues found in 1 source file
Mypy acts like this because the lambda
could be used in several contexts.
You can fix this by typing the variable that stores the lambda
:
from collections.abc import Callable
numbers = [1, 2, 3]
double: Callable[[int], int] = lambda x: x * 2 # not advised
doubled = map(double, numbers)
…but at that point you’re better off writing a function.
PEP 8 also recommends you don’t assign lambda
s to variables:
Always use a def statement instead of an assignment statement that binds a lambda expression directly to an identifier
Correspondingly, pycodestyle (run by flake8) has an error code for such assignments:
E731 do not assign a lambda expression, use a def
Fair enough.
Mypy doesn’t have a way to detect assignments of lambdas, exactly. But its disallow_any_expr
option will prevent assigning lambda
s to untyped variables, since it prevents use of all types containing Any
. For example, running with that option on the first example in this section, you’ll see:
$ mypy --disallow-any-expr example.py
example.py:2: error: Expression type contains "Any" (has type "Callable[[Any], Any]")
example.py:2: error: Expression has type "Any"
example.py:3: error: Expression type contains "Any" (has type "map[Any]")
example.py:3: error: Expression type contains "Any" (has type "Callable[[Any], Any]")
Found 4 errors in 1 file (checked 1 source file)
It shows errors for both the assignment of the lambda
, and that its use results in doubled
being a map[Any]
.
disallow_any_expr
is heavily restrictive though, and thus not normally feasible. It isn’t even included in strict mode.
Inference works in custom functions
Mypy uses its lambda inference in non-builtin functions that take Callable
types as well. For example, you could implement a custom version of map like so:
from collections.abc import Callable
from typing import TypeVar
T = TypeVar("T")
R = TypeVar("R")
def eager_map(
fn: Callable[[T], R],
items: list[T],
) -> list[R]:
return [fn(i) for i in items]
numbers = [1, 2, 3]
doubled = eager_map(lambda x: x * 2, numbers)
reveal_type(doubled)
…and Mypy can infer the type of doubled
just fine:
$ mypy example.py
example.py:14: note: Revealed type is "builtins.list[builtins.int]"
Success: no issues found in 1 source file
Good good good.
Make your development more pleasant with Boost Your Django DX.
One summary email a week, no spam, I pinky promise.
Related posts: