In Python, a function is an object like any other: you can store it in a list, pass it to another function, return it from a factory and give it attributes. lambda is simply the compact syntax for building one of those objects inline - which makes it a tool for expressing small transformations, not a shortcut for skipping def.
What does a lambda expression actually create?
The lambda keyword builds a function object with no name, no docstring and no statements beyond a single expression. It is the same kind of object a def creates - which you can see by inspecting it.
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
double = lambda n: n * 2
print(add.__name__) # add
print(double.__name__) # <lambda>
print(add.__doc__) # Add two numbers.
print(callable(double)) # True
print(double(21)) # 42
Note the missing name. A lambda has no name of its own, which is why tracebacks show <lambda> and why an error inside one is hard to locate. It is also why a lambda cannot contain a docstring, cannot contain a statement such as for or if with a block, and cannot be assigned a docstring at all.
Give a function a name and you gain three things for free: a meaningful entry in a traceback, a place to attach a docstring, and the ability to call it from anywhere else without passing it around. That is why a def with a short body is not more verbose than a lambda in any meaningful sense - it is only longer on the screen.
The practical rule that falls out of this: use a lambda when the function is small, obvious, and used right where it is written. The moment it needs a name, a docstring, more than one expression, or a conditional branch, write a def.
Where do functions as values actually earn their keep?
The biggest win is passing behaviour into code that already knows how to apply it. Sorting is the everyday example: the key argument takes a function and applies it to every element before comparing.
from dataclasses import dataclass
from operator import attrgetter
@dataclass
class Runner:
"""A runner and the time they took."""
name: str
seconds: float
runners = [
Runner("ada", 12.4),
Runner("grace", 11.9),
Runner("linus", 12.1),
]
print([r.name for r in sorted(runners, key=attrgetter("seconds"))])
print(min(runners, key=attrgetter("seconds")).name)
print(max(runners, key=lambda r: r.seconds).name)
The first line sorts by finishing time, so the result is grace, linus, ada. The second asks the same question a different way - min with a lambda instead of attrgetter - and returns grace. Both styles appear in real code; operator.attrgetter is faster because it is implemented in C, while the lambda is easier to read when the key is a computation rather than a single attribute.
The second everyday win is a higher-order function: a function that takes another function as an argument.
from collections.abc import Callable
def apply_twice(value: int, func: Callable[[int], int]) -> int:
"""Apply func to value, then apply func to the result."""
return func(func(value))
print(apply_twice(3, lambda n: n * 2)) # 12
print(apply_twice(3, lambda n: n + 5)) # 13
Every callback-driven library you will meet - map, filter, sorted, json.dumps's default hook, GUI event handlers - works this way. The lambda is just the shortest way to hand over the behaviour.
One caution about map and filter: a list comprehension is usually clearer than either of them. [f(x) for x in items] is easier to read than list(map(f, items)), and it is usually faster because it avoids the extra call layer. Reach for map when the function already exists as a named object and a comprehension would add nothing.
How do you bind arguments without writing a new function?
When you want "the same function, but with one argument already filled in", functools.partial does it without you writing a wrapper at all.
from functools import partial
def connect(host: str, port: int, timeout: float = 5.0) -> str:
"""Return a description of the connection."""
return f"{host}:{port} timeout={timeout}"
local = partial(connect, "localhost")
print(local(8080))
print(local(8080, timeout=1.5))
print(local.func.__name__)
print(local.args)
A partial is a real function object, not a stringly-typed trick: local.func is the original connect and local.args is the tuple of arguments already bound. Keyword arguments can be overridden at call time, which is why local(8080, timeout=1.5) produces a different timeout than local(8080).
For the common "sort by a key" case the standard library offers a family of shortcuts in the operator module: attrgetter for attributes, itemgetter for dictionary keys and tuple elements, and methodcaller for calling a method.
Should you use a lambda or a def?
| Situation | Use | Why |
|---|---|---|
A one-line key for sorted or max | lambda | Nothing else fits on the line, and the key is obvious from context. |
| Passing behaviour to a callback | Either | Use a lambda inline for one-off behaviour; use a def when the same callback is passed in several places. |
| Anything with a condition or a loop inside | def | A lambda cannot hold a block, and squeezing a branch into a conditional expression hurts more than it saves. |
| Anything you will want to debug later | def | Tracebacks show <lambda> instead of a name and a line number. |
| Returning a function from a factory | def | The returned function deserves its own name, docstring and tests. |
There is one more trap worth naming: a lambda closes over variables by reference, not by value. If the variable changes later, the lambda sees the new value. That is the subject of the next lesson, and it is the single most common source of "why does my callback use the wrong value" bugs.
Quick reference
- A lambda is an anonymous function object with a single expression - no statements, no docstring, no name.
- Pass functions to
sorted,min,max,map,filterand any callback API. - Use
operator.attrgetterfor plain attribute keys and a lambda when the key needs computing. - Use
functools.partialto pre-bind arguments instead of writing a wrapper function. - Promote any lambda that needs a name, a docstring or a branch into a real
def.
Try it yourself: Write a list of five dictionaries, each with name, score and joined keys. Sort it by score with a lambda, then by name with operator.itemgetter. Next, build a partial from a function that takes a base and an exponent, and use it to produce three different powers. Finally, pick the lambda you wrote first and convert it to a def - then decide which version you would rather maintain.