305

Currently, in Python, a function's parameters and return types can be type hinted as follows:

def func(var1: str, var2: str) -> int:
    return var1.index(var2)

Which indicates that the function takes two strings, and returns an integer.

However, this syntax is highly confusing with lambdas, which look like:

func = lambda var1, var2: var1.index(var2)

I've tried putting in type hints on both parameters and return types, and I can't figure out a way that doesn't cause a syntax error.

Is it possible to type hint a lambda function? If not, are there plans for type hinting lambdas, or any reason (besides the obvious syntax conflict) why not?

4
  • I think that the usual use-case for a lambda is nested within another function (contrasted with regular functions which are defined in one place and called somewhere entirely different). If you already know the types in that function, then (in principle), it should be pretty easy to figure out what types that lambda is going to receive. Additionally, modifying the syntax to support annotations would just make your lambda harder to read. Commented Nov 20, 2015 at 19:06
  • 3
    Why would you want to do that? The lambda syntax is for "throw-away" functions for use within very constrained contexts, for example, as the key argument to the sorted built-in. I really do not see a point in adding type hints in such limited contexts. In addition, using PEP-526 variable annotations to add type hints to lambda completely misses the point, IMHO. The lambda syntax is intended to define anonymous functions. What's the point of using lambda and immediately bind it to a variable? Just use def! Commented Mar 30, 2020 at 21:15
  • 7
    @LucianoRamalho (1) autocomplete, (2) IDE-assisted refactoring, (3) static analysis Commented Jun 4, 2023 at 19:36
  • 2
    I feel that the func = bit threw the whole discussion into a completely irrelevant direction. Eight years later, the problem remains: there is no way to typehint a lambda and make sure that the static type checker complains about a non-existent index function in var1. Commented Feb 16, 2024 at 12:08

7 Answers 7

337

You can, sort of, in Python 3.6 and up using PEP 526 variable annotations. You can annotate the variable you assign the lambda result to with the typing.Callable generic:

from typing import Callable

func: Callable[[str, str], int] = lambda var1, var2: var1.index(var2)

This doesn't attach the type hinting information to the function object itself, only to the namespace you stored the object in, but this is usually all you need for type hinting purposes.

However, you may as well just use a function statement instead; the only advantage that a lambda offers is that you can put a function definition for a simple expression inside a larger expression. But the above lambda is not part of a larger expression, it is only ever part of an assignment statement, binding it to a name. That's exactly what a def func(var1: str, var2: str): return var1.index(var2) statement would achieve.

Note that you can't annotate *args or **kwargs arguments separately either, as the documentation for Callable states:

There is no syntax to indicate optional or keyword arguments; such function types are rarely used as callback types.

That limitation does not apply to a PEP 544 protocol with a __call__ method; use this if you need a expressive definition of what arguments should be accepted. You need Python 3.8 or install the typing-extensions project for a backport:

from typing_extensions import Protocol

class SomeCallableConvention(Protocol):
    def __call__(self, var1: str, var2: str, spam: str = "ham") -> int:
        ...

func: SomeCallableConvention = lambda var1, var2, spam="ham": var1.index(var2) * spam

For the lambda expression itself, you can't use any annotations (the syntax on which Python's type hinting is built). The syntax is only available for def function statements.

From PEP 3107 - Function Annotations:

lambda 's syntax does not support annotations. The syntax of lambda could be changed to support annotations, by requiring parentheses around the parameter list. However it was decided not to make this change because:

  • It would be an incompatible change.
  • Lambda's are neutered anyway.
  • The lambda can always be changed to a function.

You can still attach the annotations directly to the object, the function.__annotations__ attribute is a writable dictionary:

>>> def func(var1: str, var2: str) -> int:
...     return var1.index(var2)
...
>>> func.__annotations__
{'var1': <class 'str'>, 'return': <class 'int'>, 'var2': <class 'str'>}
>>> lfunc = lambda var1, var2: var1.index(var2)
>>> lfunc.__annotations__
{}
>>> lfunc.__annotations__['var1'] = str
>>> lfunc.__annotations__['var2'] = str
>>> lfunc.__annotations__['return'] = int
>>> lfunc.__annotations__
{'var1': <class 'str'>, 'return': <class 'int'>, 'var2': <class 'str'>}

Not that dynamic annotations like these are going to help you when you wanted to run a static analyser over your type hints, of course.

Sign up to request clarification or add additional context in comments.

9 Comments

If the answer is func: Callable[[str, str], int] = lambda var1, var2: var1.index(var2) then why isn't a better answer def func(var1: str, var2: str) -> int: return var1.index(var2)???
@GuidovanRossum: because that's the answer to a different question. :-) The question here was how to apply type hinting to a lambda that gives the same result as defining a function. Still, I'll add a section covering why you might want to not use a lambda.
No, taken literally the answer should be "No, it is not possible, there are no plans to change this, and the reasons are primarily syntactical --and it's easy enough to define a named function and annotate that." I also note that the proposed workaround (create a variable with a specific Callable type) is not very helpful if the lambda is buried inside a large call or data structure, so it's not in any sense "better" than defining a function. (I'd argue that it's worse because the Callable notation is not very readable.)
@GuidovanRossum: Can I invite you to write your own answer then? With your specific status as father of the language, an answer by you would easily outstrip this one, given time, and you get to write exactly what you feel should be in it.
For the purpose of defining a typed function variable, along with a default (perhaps a noop), I think the above answer is reasonable (though ugly thanks to Callable syntax) code
|
69

Since Python 3.6, you can (see PEP 526):

from typing import Callable
is_even: Callable[[int], bool] = lambda x: (x % 2 == 0)

4 Comments

I don't understand this answer: where do you set the type of x?
@stenci The is_even function is a Callable that expects one int argument, so x is an int.
this isn't actually annotating the lambda itself: you can't retrieve these annotations from the lambda object as you can for an annotated function
mypy 0.701 with Python 3.7 correctly typechecks this: is_even('x') causes a type error.
20

I strongly dislike assigning a lambda function to an identifier (See Is it pythonic: naming lambdas). If you want to do that nonetheless, you already have answers here.

If you don't, and just want the type-checkers to do their thing, the answer is typing.cast:

from typing import cast, Callable

cast(Callable[[int], int], lambda x: x + 1)("foo")
# error: Argument 1 has incompatible type "str"; expected "int"

Comments

12

For those just looking for a swift access to intellisense when writing your code, an almost-hack is to type-annotate the parameter just before the lambda declaration, do your work and only then shadow it with the parameter.

x: YourClass
map(lambda _:  x.somemethod ...)  # x has access to methods defined on YourClass

Then, right after:

x: YourClass # can remove or leave
map(lambda x:  x.somemethod, ListOfYourObjects)  # inner x now shadows the argument

Comments

7

You can define something really fancy like this since Python 3.12:

from collections.abc import Callable

class typed[*Ts]:
    def __new__[U](cls, f: Callable[[*Ts], U], /) -> Callable[[*Ts], U]:
        return f

add1point5 = typed[int](lambda x: x + 1.5)  # (int) -> float
# You can see that the return type is automatically inferred,
# so you only need to specify types of the parameters
print(add1point5(42))

# And an example with multiple parameters
concat = typed[str, str](lambda s1, s2: s1 + s2)  # (str, str) -> str
print(concat("foo", "bar"))

The usage of the __new__ magic method here is intentional, allowing you to manually specifying only the parameter types, leaving the return type to be inferred.

It is also possible to use this function in Python 3.11, but the syntax gets a bit ugly:

from collections.abc import Callable
from typing import Generic, TypeVar, TypeVarTuple

Ts = TypeVarTuple("Ts")
U = TypeVar("U")

class typed(Generic[*Ts]):
    def __new__(cls, f: Callable[[*Ts], U], /) -> Callable[[*Ts], U]:
        return f

And in lower versions (where TypeVarTuple is not available):

from collections.abc import Callable
from typing import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")
# ...
R = TypeVar("R")

class typed1(Generic[T]):
    def __new__(cls, f: Callable[[T], R], /) -> Callable[[T], R]:
        return f

class typed2(Generic[T, U]):
    def __new__(cls, f: Callable[[T, U], R], /) -> Callable[[T, U], R]:
        return f

# ...

Comments

2

I found it ugly (or should I say unclear) putting the type hint in the same line of the code, so it is nice (more readable) to split it into 2 lines.

all in one line:

from typing import Callable

a: Callable[[int], int] = lambda x : x + 10

split onto 2 lines:

from typing import Callable

b : Callable[[int], int]
b = lambda x : x + 20

This is especially true when there are more than one input parameters to hint.

Comments

0

I am leaving a working example in case someone needs it. I have implemented a simple retry function with the help of the above posts.

from typing import TypeVar, Callable

import time

T = TypeVar('T')


def retry(attempts: int, delay: float, func: Callable[[], T]) -> T:
    for i in range(attempts):
        try:
            return func()
        except Exception as e:
            time.sleep(delay)
            if i == attempts-1:
                raise Exception(e)
                
def some_raise():
    raise Exception('20')
    
val = retry(3, 0.05, lambda: some_raise())
print(val)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.