A Guide to Python Lambda Functions
In Python, Lambda functions are rare compared to “normal” functions, and occasionally misunderstood or overused.
In this article we’ll cover what Lambda functions are, how to use them, and how they compare with normal functions and other alternatives. We’ll also touch on the history of lambda functions, such as where the name “lambda” came from and why Python’s creator Guido van Rossum wanted to remove them.
We’ll cover:
- What Lambda Functions Are (in Python)
- Examples of Using Lambda Functions in Python
- Alternatives to Lambda Functions
What are Lambda Functions (in Python)?
In Python, the lambda
keyword allows you to define a lambda function. This is a function that returns a single expression in one line. It’s like a shortcut for using def
to create a normal function, which requires at least two lines.
For example, we could define a simple addition function using def
like so:
This takes two lines.
Using lambda
we can instead write our function as:
This takes only one line.
To convert the syntax, we:
- Replaced
def
withlambda
. - Removed the parentheses around the arguments.
- Put the function body on the same line as its definition, after the single colon (
:
). - Removed the
return
keyword. Lambda functions always return the result of their one expression. - Added an explicit assignment of the function to the name
add_lambda
.
Despite all these syntactical differences, the two versions work identically:
Why They’re Called Lambda Functions
Lambda, or λ, is the 11th letter of the Greek alphabet. Due to the use of the Greek alphabet in mathematics, Alonzo Church ended up using it in the 1930’s when describing a concept he called Lambda calculus. This is a formal system describing any possible computation - something like a purely mathematical programming language.
Lambda calculus is so-called because it uses Lambda (λ) to represent functions, which also never have names. The Lisp programming language copied this concept, and Python copied it from Lisp.
Examples of Using Lambda Functions in Python
The main motivation for using lambda
is to create a function and use it as an argument in the same line. These are often done with several built-in functions that take functions as arguments. Let’s look at three examples now.
For our examples, let’s use a list of puppies with their cuteness ratings:
We’ll manipulate the puppies
list with some Python built-ins, to which we will pass lambda functions.
Use With list.sort()
and sorted()
The list.sort()
method takes with an optional key
argument. This is a function to map the list items to values to sort them by. We can use it to sort our puppies by their increasing cuteness ratings, by passing key
as a function that extracts a given puppy’s cuteness value.
Using def
, we need to define the function separately before we call list.sort()
:
Using lambda
, we can define the function inside the call to sort()
directly:
The lambda
version is only one line, whilst the def
version is three lines (four if you count the blank line between the function and call to list.sort()
).
We can make it even shorter by using a one letter variable name inside the lambda function:
The sorted()
built-in similarly takes a key
argument, but it takes with any iterable instead of just lists, so you’ll often see lambda
used in conjunction with it. For example:
Use With filter()
The filter()
built-in takes a function and an iterable, and returns the items from the iterable for which the function returned true. We can use it to filter our puppies to only the cutest ones, by passing key
as a function that returns if a given puppy has enough cuteness.
Using def
, we again need to define the function separately, before we call filter()
:
(Note we need to call list()
on filter()
to see its results, because it is a generator.)
Using lambda
, we can again define the function in the same line as its use:
Again, we’ve saved a few lines.
Use With map()
The map()
built-in takes a function and an iterable, and returns a new iterable of the results of applying the function on the items in the passed iterable. We can use it to extract our puppies’ names into a list of strings.
Using def
, we again need to define the function separately, before we call filter()
:
(Note we again need to call list()
on map()
to see its results, because it is also a generator.)
Using lambda
, we can once again define the function in the same line as its use, saving some lines:
Alternatives to Lambda Functions
The existence of lambda functions in Python is somewhat controversial. The creator of Python, Guido van Rossum, even advertised his intention to remove it in Python 3.0, along with filter()
and reduce()
. In his 2005 post The fate of reduce() in Python 3000, he wrote:
About 12 years ago, Python aquired lambda, reduce(), filter() and map()… But, despite of the PR value, I think these features should be cut from Python 3000.
(Python 3000 was the working name for Python 3.0.)
Ultimately Python kept lambda
for backwards compatibility, and Guido updated the post with:
lambda, filter and map will stay (the latter two with small changes, returning iterators instead of lists). Only reduce will be removed from the 3.0 standard library. You can import it from functools.
But there are still alternatives to using a lambda
function, and they are preferable for many use cases. Let’s look at those now.
Normal Functions
The first alternative is to use a normal function. We already compared these with their corresponding lambda functions in our three examples above. Normal functions have a number of advantages that the lambda
syntax does not allow.
a) Normal Function Advantage 1 - Naming
Normal functions have a name, which allows us to clarify our intention. With a complex lambda function you might find yourself writing a comment to describe what it does. Using a normal function you can embed this informatino in the function’s name itself.
Take our filter()
example again. Imagine the filtering we did was because there’s a minimum of cuteness of 100 to enter a contest. We might try embed this in the lambda function version with a comment, which requires us to split filter()
across multiple lines:
But with a normal function, we can put that information in the function name:
Note we can give lambda functions names too by assigning them:
But if you check this function’s __name__
attribute, you’ll see it’s actually called <lambda>
:
All lambda functions have the name '<lambda>'
, even after we assign them to variables. This is because Python doesn’t have any name information when creating the function. This will appears in various code inspection tools, including stack traces, and can make debugging a little harder.
Normal Function Advantage 2 - Expression Splitting
Our previous examples all used short functions, so the lambda
syntax was readable on a single line. But if our function contained a longer expression, using a lambda
function could mean cramming lots of code on one line.
Imagine we wanted to sort our puppies in a more complex way: in reverse order, by the upper-cased first letter of the last part of their names. Using lambda
, our call to list.sort()
would look like this:
This line contains a lot of different pieces. I count 14 different object names, argument names, keywords, and values, plus a lot of punctuation. That’s a lot to read and understand at once!
We could improve the readability a bit by splitting the code over multiple lines:
But then we have given up some of the benefit of using lambda
, as we have the same number of lines of code as if we hadn’t used it. The lambda function is also still quite a lot of steps to understand.
By using a normal function, we can split the expression in two pieces, and assign a name to the intermediate last_name
varible:
Now we can much more easily follow the calculation, and we’ve again used the function name to clarify our intention.
Normal Function Advantage 3 - Clearer Decorators
Python’s decorator syntax allows us to extend the behavior of a function with a wrapper. When declaring a function with lambda
, it’s still possible to use decorators, but without the @
syntax which highlights use as a decorator.
For example, if we found that our “is cute enough” check was taking a significant amount of time, we could add caching with the functools.lru_cache
decorator from the standard library. Using a normal function, we can add it with the @
syntax:
When using lambda
we have to call the decorator ourselves, without @
:
This works, but it slightly obscures lru_cache
being a decorator.
Normal Function Advantage 4 - Function Annotations
Python’s function annotations allow us to add for type hints. Such hints declare the expected types of variables and we can verify our expectations with a type checker tool such as mypy. These let us make extra guarantees of our code’s correctness, alongside tests.
Unfortunately, because function annotations use colons (:
) as their separator, they are not compatible with lambda
. lambda
already uses a single colon to separate its arguments from its expression, so there’s nowhere to add annotations.
For example, we could annotate our previous is_cute_enough_for_contest()
function like so:
This declares that we expect it to take a Puppy
object and return a bool
. We could run mypy to check that is_cute_enough_for_contest()
is always called with such types.
If we try to add such annotations to a lambda
, we’ll only get a SyntaxError
:
This is because the first colon starts the function body.
Normal Function Advantage 5 - Accurate Test Coverage
One tool for ensuring your tests check all parts of your system is to measure test coverage with coverage.py. This works on a line-by-line basis.
Because lambda functions include their body on the same line as their definition, they will show as fully covered, even if the function is never called. Thus, you might miss bugs in your lambda functions’ bodies, such as mistyping an attribute name.
Normal functions aren’t subject to this problem, because their body starts on a separate line to their declaration. If they don’t get called during tests, coverage will always show them as uncovered.
List Comprehensions (And Other Types)
The second alternative to many of the uses of lambda
is to use a comprehension. Many of the built-in functions that lambda functions are typically used with use the passed function to generate a new list of items, so a list comprehension is appropriate. But your use case might mean using a set or dict comprehension, or a generator expression.
For example, any call to filter()
can be rewritten with an equivalent list comprehension. Take our cuteness filter()
call:
We can rewrite it using a list comprehension as:
We can also put the condition inside the comprehension:
Using a comprehension without a function call like this is even a little bit faster. This is because the expression uses local variable access, rather passing them into another function with its own namespace.
Similarly, any map()
call can be rewritten as a list comprehension. Again, take our previous map()
example:
We can rewrite this as:
This is again simpler.
Comprehensions offer quite flexible syntax, allowing the same things that a for
loop would, and so they’re more generally useful than filter()
and map()
.
The operator
Module
A third alternative to writing lambda functions is to use the standard library’s operator
module. This module contains some predefined functions and function factories, which can replace the most common use cases for lambda functions. Let’s look at both of these separtaely, factories first.
Function Factories
The function factories offered by operator
create functions that return a value based on their input. These can replace a lot of common use cases for lambda functions.
Recall the lambda function we used with list.sort()
:
We can construct an identical function with the operator.attrgetter
function factory. We pass it the name the attribute we want to extract, and it returns a function that does so:
We can use this inline in our list.sort()
example:
The “sort by an attribute” pattern is quite common, so attrgetter
is often used to implement it.
Another note: the function that attrgetter
returns is implemented in C, so it’s slightly faster than using either a normal or lambda function.
The operator
module also offers two other function factories. itemgetter
can replace a lambda that gets an item with []
, such as lambda puppies: puppies[0]
. And methodcaller
can replace a lambda that calls a method, such as lambda puppy: puppy.bark()
.
Operator Fucntions
Another set of functions offered by the operator
module are its wrappers of Python’s operators (hence the module’s name). These can replace another bunch of common use cases for lambda functions.
For example, imagine we had a mega-cuteness formula that required us to multiply our puppies’ extracted cuteness values together. We could use functools.reduce()
function with a lambda function to do this. reduce()
will take pairs of puppies
Our lambda function, lambda a, b: a * b
, is equivalent to operator.mul
(short for multiply). So we could also write:
Again because the operator
function is implemented in C, it is slightly faster than our handwritten version.
The operator module provides wrapper functions for all of Python’s operators, so there’s no need to write such lambda functions.
Fin
I hope this guide to lambda functions has answered many of your questions about them. May your Python be ever more readable,
—Adam