Python Type Hints - Lambdas don’t support type hints, but that’s okay
Python has no syntax to add type hints to
lambdas, 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
lambdas, based on where they’re used.
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
double = lambda x: x * 2 reveal_type(double)
…and Mypy tells says it takes and returns
$ 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
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…
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
$ 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() returns the its own lazy iterable type).
If Mypy didn’t do this inference, it would only be able to declare
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)
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)
doubled has type
$ 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
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
lambdas 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
Mypy doesn’t have a way to detect assignments of lambdas, exactly. But its
disallow_any_expr option will prevent assigning
lambdas 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
disallow_any_expr is heavily restrictive though, and thus not normally feasible. It isn’t even included in strict mode.
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.
- Python Type Hints - How to Narrow Types with isinstance(), assert, and Literal
- Python Type Hints - How to Split Types by Python Version
- Python Type Hints - Mypy doesn’t allow variables to change type
Tags: mypy, python