On this page
A Python lambda function is a small anonymous function written as a single expression: lambda x: x ** 2. It returns the value of that expression automatically. Use it where a short function is passed straight to another function, such as a sort key, and use def everywhere else.
Lambdas look simple, which is how they cause trouble. Three lambdas built in a loop can all return 12 when you expected 10, 11 and 12, and a lambda over a large DataFrame can be far slower than one vectorised line. This guide is for Python learners who already write functions with def and want to know when the short form is worth it. It covers the best uses, when def is clearer, the loop gotcha, and the tools that often replace a lambda.
Lambdas are the last stop in the functions series; if def, arguments and scope are still new, start with user-defined Python functions.
What is a lambda in Python?
A lambda is a function without a name, limited to a single expression. The official tutorial's lambda expressions section describes them as small anonymous functions:
square = lambda x: x ** 2 # works, but see below
square(4) # 16
# equivalent
def square(x):
return x ** 2
The return is implicit. As a result, there is no room for statements, loops, assignments, annotations or a docstring. Writing lambda x: return x is a SyntaxError.
That constraint is the whole design. Lambdas exist for the case where naming the function would be more noise than signal.
Lambda vs def at a glance
lambda |
def |
|
|---|---|---|
| Kind | expression | statement |
| Body | one expression | any number of statements |
| Return | implicit | explicit return |
| Name in tracebacks | <lambda> |
the real function name |
| Docstring and type hints | no | yes |
| Best for | short callbacks and key functions | everything else |
Where does a Python lambda function actually help?
Sort keys are the single best use:
people = [("Ananya", 28), ("Rohit", 35), ("Meera", 24)]
sorted(people, key=lambda p: p[1]) # by age
# [('Meera', 24), ('Ananya', 28), ('Rohit', 35)]
sorted(people, key=lambda p: p[1], reverse=True)
# [('Rohit', 35), ('Ananya', 28), ('Meera', 24)]
sorted(people, key=lambda p: (p[1], p[0])) # age, then name
That last one — a tuple key for multi-level sorting — is a pattern worth remembering. The sorting HOWTO notes that the key function is called exactly once per item, so even a lambda key stays fast.
Sorting dictionaries and objects works the same way:
scores = {"Ananya": 91, "Rohit": 78, "Meera": 85}
sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
# [('Ananya', 91), ('Meera', 85), ('Rohit', 78)]
from collections import namedtuple
Employee = namedtuple("Employee", "name salary")
employees = [Employee("Ananya", 95000), Employee("Rohit", 72000)]
sorted(employees, key=lambda e: e.salary)
# [Employee(name='Rohit', salary=72000), Employee(name='Ananya', salary=95000)]
However, you do not always need a lambda. When a named function already does the job, pass it directly:
words = ["kiwi", "Banana", "fig", "apple"]
sorted(words) # ['Banana', 'apple', 'fig', 'kiwi'] - uppercase first
sorted(words, key=str.lower) # ['apple', 'Banana', 'fig', 'kiwi']
max(scores, key=scores.get) # 'Ananya' - the key with the highest value
min, max and aggregates:
records = [{"name": "a", "score": 90}, {"name": "b", "score": 75}]
max(records, key=lambda r: r["score"]) # {'name': 'a', 'score': 90} - the whole record
min(words, key=len) # 'fig'
Returning the item rather than the value is why key= beats a comprehension here.
pandas apply, for small transformations:
df["initial"] = df["name"].apply(lambda s: s[0].upper())
df["band"] = df["score"].apply(lambda x: "high" if x > 80 else "low")
Note the performance caveat below, though.
Callbacks and default factories:
from collections import defaultdict
counts = defaultdict(lambda: 0)
nested = defaultdict(lambda: defaultdict(list))
nested["north"]["q1"].append(1200)
defaultdict(lambda: defaultdict(list)) is an idiomatic use, because you need a callable and naming it would not help.
When should you use def instead?
Never assign a lambda to a name. PEP 8's programming recommendations say to always use def instead, and the reason is practical:
# don't
calculate_tax = lambda amount, rate: amount * rate
# do
def calculate_tax(amount, rate):
return amount * rate
The def version has a real __name__, so tracebacks say calculate_tax rather than <lambda>. In a stack trace full of <lambda> entries, that matters.
Use def when the logic needs explaining:
# unreadable
process = lambda r: (r["a"] * 2 + r["b"]) / (r["c"] if r["c"] else 1)
# clear
def normalised_score(record):
"""Combine a and b, normalised by c (guarding division by zero)."""
combined = record["a"] * 2 + record["b"]
divisor = record["c"] or 1
return combined / divisor
normalised_score({"a": 3, "b": 4, "c": 0}) # 10.0
Use def when you need more than an expression. There is no try, no loop and no assignment statement inside a lambda. So if you are reaching for a workaround, use def.
Why do lambdas created in a loop share one value?
Because a closure looks up the loop variable when it runs, not when it is created. This catches everyone once:
funcs = []
for i in range(3):
funcs.append(lambda x: x + i)
[f(10) for f in funcs] # [12, 12, 12] - not [10, 11, 12]
Closures look up the variable when they run, not its value when they were created. By the time the lambdas run, i is 2. The Python FAQ covers this exact case: why lambdas defined in a loop all return the same result. The same thing happens with def inside a loop, so it is not a lambda-only quirk.
Bind the value explicitly with a default argument, which is evaluated at definition time:
funcs = [lambda x, i=i: x + i for i in range(3)]
[f(10) for f in funcs] # [10, 11, 12]
Alternatively, use functools.partial, which is clearer about intent:
from functools import partial
def add(x, i):
return x + i
funcs = [partial(add, i=i) for i in range(3)]
[f(10) for f in funcs] # [10, 11, 12]
Should you use map and filter with a lambda?
Usually not. Lambdas are often taught alongside map and filter, but in Python a comprehension usually reads better:
nums = [1, 2, 3, 4, 5]
list(map(lambda x: x ** 2, nums)) # [1, 4, 9, 16, 25]
[x ** 2 for x in nums] # same, more idiomatic
list(filter(lambda x: x % 2 == 0, nums)) # [2, 4]
[x for x in nums if x % 2 == 0] # same, more idiomatic
map earns its place when you already have a named function and no transformation to express:
lines = [" a ", "b "]
list(map(str.strip, lines)) # ['a', 'b'] - no lambda needed
list(map(int, ["1", "2"])) # [1, 2]
When is operator.itemgetter better than a lambda?
Whenever the lambda only fetches an item or an attribute. For simple attribute or index access, the operator module is clearer, and the sorting HOWTO describes its accessors as easier and faster:
from operator import itemgetter, attrgetter
sorted(people, key=itemgetter(1)) # instead of lambda p: p[1]
sorted(employees, key=attrgetter("salary")) # instead of lambda e: e.salary
sorted(records, key=itemgetter("score", "name")) # multi-key
Are lambdas slow in pandas apply?
The lambda itself is not slow, but apply with a lambda runs Python code once per row. On large frames, therefore, use vectorised operations instead:
# slow on a million rows
df["total"] = df.apply(lambda r: r["price"] * r["qty"], axis=1)
# fast - vectorised
df["total"] = df["price"] * df["qty"]
# conditional, vectorised
import numpy as np
df["band"] = np.where(df["score"] > 80, "high", "low")
The lambda is not the problem; row-wise Python is. Our NumPy broadcasting guide covers why vectorisation is so much faster.
Common mistakes with lambdas
- Assigning a lambda to a name instead of writing
def. - Late binding in loops, which gives every lambda the final loop value.
- Wrapping an existing function, as in
key=lambda s: s.lower()whenkey=str.lowerworks. - Cramming conditionals and arithmetic into one unreadable line.
- Using
applywith a lambda on large DataFrames when a vectorised expression exists.
When should you use a Python lambda function?
Use a Python lambda function when it is an argument to another function and the logic fits comfortably on one line. Everywhere else, use def.
The Sunday Growth Brief
One email a week: the best new comparisons, a fresh roadmap and the tech news worth your attention.
No spam. Unsubscribe in one click.
Related reading
User-defined functions covers function design properly. Iterators and generators covers the lazy-evaluation companion.
Frequently asked questions
What is the difference between lambda and def?
A lambda is an expression that produces a function, limited to a single expression with an implicit return. def is a statement that can contain any code, accepts annotations and a docstring, and gives the function a real name that appears in tracebacks. Both create the same kind of function object, so anything a lambda does, def can do too.
Should I assign a lambda to a variable?
No. PEP 8 says to always use a def statement instead of binding a lambda directly to a name. You get the same function object with a meaningful __name__ for debugging, and you keep the option to add a docstring and type hints later. Named lambdas save one line and cost clarity, so they are a habit worth dropping.
Why do lambdas in a loop all return the same value?
Because closures look up variables when the function runs, not when it is defined. All the lambdas share the loop variable, which holds its final value by the time they are called. Bind the current value with a default argument, as in lambda x, i=i: x + i, or use functools.partial. Functions defined with def in a loop behave the same way.
Are lambdas slower than regular functions?
No. A lambda and an equivalent def compile to essentially the same bytecode, so calling them costs the same. Any perceived difference comes from how they are used: calling a Python function once per row over a large DataFrame is slow whether it is a lambda or a def, and vectorised operations are the real fix.
Written by
Ayushi Kulshreshta
AI Engineer
AI engineer building retrieval-augmented systems, previously a software developer at VDB Inc. and an R&D associate at Nokia.

