233

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?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Nathan Merrill
  • 7,648
  • 5
  • 37
  • 56
  • 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. – mgilson Nov 20 '15 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`! – Luciano Ramalho Mar 30 '20 at 21:15
  • @LucianoRamalho (1) autocomplete, (2) IDE-assisted refactoring, (3) static analysis – obe Jun 04 '23 at 19:36

7 Answers7

270

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.

Uyghur Lives Matter
  • 18,820
  • 42
  • 108
  • 144
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 6
    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)`??? – Guido van Rossum Jul 05 '20 at 22:47
  • 9
    @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. – Martijn Pieters Jul 06 '20 at 20:02
  • 9
    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.) – Guido van Rossum Jul 07 '20 at 21:21
  • 2
    The solution also distracts by looking like it's defining a *variable* with a certain Callable type that could be assigned different lambdas during its lifetime. That's different than defining a function (at least mypy balks if you redefine or reassign a def but not a variable of Callable type) and further away from the original question "I just want a lambda with type annotations." I continue to claim that there is no benefit in keeping the lambda form instead of the def form in this case. (And I would say that even if there were no type annotations.) – Guido van Rossum Jul 07 '20 at 21:22
  • 18
    @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. – Martijn Pieters Jul 08 '20 at 21:14
  • 3
    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 – Abram Apr 21 '21 at 21:37
  • 3
    With respect to Guido, I disagree that this is not a proper answer to the question. I think this answer perfectly answers about how to annotate lambdas in regards to their limitations. I prefer this answer over "it's not possible" – Break Jul 07 '21 at 17:36
  • He's right though. At this point you're no longer annotating the lambda itself, only the variable containing it – RivenSkaye Sep 27 '22 at 11:50
67

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

from typing import Callable
is_even: Callable[[int], bool] = lambda x: (x % 2 == 0)
jan
  • 1,408
  • 13
  • 19
  • I don't understand this answer: where do you set the type of `x`? – stenci Mar 19 '18 at 21:07
  • 3
    @stenci The `is_even` function is a `Callable` that expects one `int` argument, so x is an `int`. – jan Mar 20 '18 at 13:58
  • 1
    I understand now. At first glance I thought you were annotating only the type returned by the lambda, not the type of its arguments. – stenci Mar 20 '18 at 14:11
  • 11
    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 – c z Jun 11 '18 at 12:05
  • 3
    mypy 0.701 with Python 3.7 correctly typechecks this: `is_even('x')` causes a type error. – Konrad Rudolph May 24 '19 at 13:02
10

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
Neuron
  • 5,141
  • 5
  • 38
  • 59
rtviii
  • 807
  • 8
  • 19
7

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"
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)
Daniel Hornik
  • 1,957
  • 1
  • 14
  • 33
0

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.

D.L
  • 4,339
  • 5
  • 22
  • 45
-8

this works fine for me...

def fun()->None:
    def lam(i: int)->None:
        print("lam!", i)
    print("fun!")
    lam(1)
    lam(2)


fun()
lam()

prints

fun!
lam! 1
lam! 2
Traceback (most recent call last):
  File "/home/jail/prog.py", line 16, in <module>
    lam()
NameError: name 'lam' is not defined

tested on CPython 3.6.12 and 3.10.2

hanshenrik
  • 19,904
  • 4
  • 43
  • 89