Python Lambda Functions
Anonymous functions, used with care
A complete, runnable reference for Python's lambda keyword. Learn the syntax, when to use it with map, filter, sorted, and reduce — and just as importantly, when not to use it. Every code block includes a numbered line-by-line breakdown.
numbers = [1, 2, 3, 4, 5] double = lambda x: x * 2 evens = list(filter(lambda x: x % 2 == 0, numbers)) doubled = list(map(lambda x: x * 2, numbers)) students = [("Ana", 95), ("Bob", 88)] by_score = sorted(students, key=lambda s: s[1], reverse=True) print(double(7)) print(evens) print(doubled) print(by_score)
📚 What This Lesson Covers
Eleven sections, each with runnable code and a numbered explanation of every line
🎯 Why Lambdas?
Sometimes you need a function once, inline, and giving it a name adds noise
A lambda is an anonymous function — defined inline, in a single expression, without a name. It exists for one specific situation: when you need a tiny function passed to something else (like the key argument of sorted), and defining a whole def block would be more ceremony than the function is worth. That's it. Lambdas are not a general-purpose replacement for def, and modern Python style nudges most everyday use toward list comprehensions or named functions instead.
The one-sentence summary
A lambda is a def that returns a single expression, has no name, and is meant to be used once. Use it when the trade-off makes sense; reach for def or a comprehension when it doesn't.
key= argument of sorted/max/min, small callbacks in GUI frameworks, and sometimes inside map/filter. In most other cases, a comprehension or a named function is clearer.📝 Lambda Syntax
A single expression, parameters, no name — but perfectly callable like any function
📐 Basic Syntax
The syntax is lambda parameters: expression. The expression's value is returned automatically — no return keyword.
double = lambda x: x * 2 add = lambda a, b: a + b greet = lambda name: f"Hello, {name}!" no_args = lambda: 42 print(double(7)) print(add(3, 4)) print(greet("Ana")) print(no_args()) print(type(double).__name__)
- 1
lambda x: x * 2— takes one parameterx, returnsx * 2. The result is a function object, bound todouble. - 2Two parameters:
a, b. Returns their sum. - 3An f-string as the body works — it's a single expression.
- 4Zero parameters:
lambda:is valid, returning a fixed value. - 6–9Calling each lambda works exactly like calling a
deffunction. Results:14,7,"Hello, Ana!",42. - 10The result of
type(double).__name__is"function"— a lambda produces a real function object, not a special type.
🎯 The Single-Expression Rule
A lambda body must be a single expression. Conditional logic is allowed via ternary expressions, but loops, try/except, and multiple statements are not.
sign = lambda x: "pos" if x > 0 else "zero" if x == 0 else "neg" safe_div = lambda a, b: a / b if b else None clamp = lambda x, lo, hi: max(lo, min(x, hi)) print(sign(5)) print(sign(-3)) print(sign(0)) print(safe_div(10, 2)) print(safe_div(10, 0)) print(clamp(15, 0, 10))
- 1Chained ternary (ternaries can nest, though readability suffers past one level). Returns
"pos","zero", or"neg". - 2Conditional expression: divide if
bis truthy, else returnNone. Guards againstZeroDivisionError. - 3Single expression using built-in
maxandminto clamp. - 5–10Verify the behaviour:
"pos","neg","zero",5.0,None,10.
return, if/elif/else statements (the ternary expression is different), for loops, while, try/except, or assignment (though the walrus operator := works). If you find yourself wanting those, you want a def.🎁 Default and Variadic Arguments
Lambdas accept all the same parameter forms as def — defaults, *args, **kwargs — just without the enclosing block.
power = lambda base, exp=2: base ** exp total = lambda *nums: sum(nums) shout = lambda text, **opts: text.upper() + opts.get("suffix", "!") print(power(3)) print(power(3, 4)) print(total(1, 2, 3, 4)) print(shout("hi")) print(shout("hi", suffix="!!!"))
- 1
exp=2is a default parameter. Callingpower(3)squares 3; callingpower(3, 4)raises it to the 4th power. - 2
*numscollects all positional arguments into a tuple. - 3
**optscollects keyword arguments.opts.get("suffix", "!")returns a default if not provided. - 5–9Result:
9,81,10,"HI!","HI!!!".
⚖️ Lambda vs def — When to Use Which
Both produce function objects. The choice is about readability, not behavior
| Feature | lambda | def |
|---|---|---|
| Name | Anonymous (unless assigned) | Named |
| Number of statements | Exactly one expression | Any number |
| Return statement | Implicit (expression is returned) | Explicit return |
| Docstring | Not possible | Possible |
| Type hints | Not on parameters, not on return | Fully supported |
| Tracebacks | Show <lambda> as the function name | Show the real function name |
| Debugging | Hard to break into | Easy — set a breakpoint in the body |
| Best for | Inline one-off callbacks (e.g. key=) | Everything else |
🤔 The Same Function, Two Ways
# With def def square(x): return x * x # With lambda square_lambda = lambda x: x * x print(square(5)) print(square_lambda(5))
- 2–3The
defversion: named, with an explicit return. Two lines of code, one name. - 6The lambda version: one line, assigning the anonymous function to
square_lambda. This is often called an anti-pattern because assigning to a name defeats the purpose of anonymity —defwould be clearer here. - 8–9Both print
25.
f = lambda x: ..., write def f(x): return ... instead. The exception is when the lambda is used directly as an argument (e.g. sorted(data, key=lambda x: ...)) — no assignment, no confusion.🔄 map() and filter()
Classic functional-style transforms — though comprehensions often read better today
🔄 map() with lambda
map(func, iterable) applies func to every item. It returns a lazy iterator in Python 3 — wrap in list() to see the results.
numbers = [1, 2, 3, 4, 5] squared = list(map(lambda x: x * x, numbers)) doubled = list(map(lambda x: x * 2, numbers)) labels = list(map(lambda x: f"#{x}", numbers)) print(squared) print(doubled) print(labels) # With a named function, no lambda needed words = [" ana ", " bob", "cara "] cleaned = list(map(str.strip, words)) print(cleaned)
- 1Input list.
- 3–5Three uses of
map: square each, double each, add a#prefix. Eachmapcall returns an iterator;list(...)materialises it. - 7–9Print the three resulting lists.
- 12–13When a named function does the job —
str.stripis already a function — pass it directly. No lambda needed. Result:['ana', 'bob', 'cara'].
str.strip, int, len, math.sqrt, etc.), map is often clean. When you'd need a lambda, [x * x for x in numbers] reads better than list(map(lambda x: x * x, numbers)).🔍 filter() with lambda
filter(predicate, iterable) keeps items where the predicate returns truthy. Also returns a lazy iterator.
numbers = [1, 2, 3, 4, 5, 6] evens = list(filter(lambda x: x % 2 == 0, numbers)) greater_than_three = list(filter(lambda x: x > 3, numbers)) print(evens) print(greater_than_three) # With None as predicate: keeps truthy items mixed = [0, 1, None, "hi", "", 42] truthy = list(filter(None, mixed)) print(truthy)
- 1Input list.
- 3Keeps even numbers:
[2, 4, 6]. - 4Keeps numbers greater than 3:
[4, 5, 6]. - 9–11
filter(None, iterable)— special case: uses Python's truthiness rules instead of calling a predicate. Keeps1,"hi",42. Drops0,None,"".
[x for x in numbers if x % 2 == 0] reads better than list(filter(lambda x: x % 2 == 0, numbers)). Reserve filter for cases where the predicate is already a named function — filter(str.isdigit, chars) is arguably cleaner than a comprehension.🔀 sorted(key=...) — The Main Real-World Use
This is where lambdas genuinely earn their place in modern Python code
🔀 Sorting by a Custom Field
sorted(iterable, key=func) uses func to compute a comparison key. A lambda here is idiomatic and clear.
students = [
{"name": "Ana", "score": 95},
{"name": "Bob", "score": 88},
{"name": "Cara", "score": 92},
]
by_score = sorted(students, key=lambda s: s["score"])
by_score_desc = sorted(students, key=lambda s: s["score"], reverse=True)
by_name = sorted(students, key=lambda s: s["name"])
for s in by_score_desc:
print(f"{s['name']}: {s['score']}")
print("---")
for s in by_name:
print(s["name"])- 1–5Three student dicts.
- 7
key=lambda s: s["score"]—sortedcalls the lambda on each student and compares the results. Sorts ascending by score. - 8
reverse=Truesorts descending — highest score first. - 9Sorting by name — a different key on the same data.
- 11–12Print in descending-score order: Ana (95), Cara (92), Bob (88).
- 15–16Print in alphabetical order: Ana, Bob, Cara.
📊 Multi-Field Sorting
Return a tuple from the key function to sort by multiple fields. Python compares tuples element-by-element.
records = [
("Ana", 30, "Manila"),
("Bob", 25, "Cebu"),
("Cara", 30, "Davao"),
("Dan", 25, "Manila"),
]
# Sort by age ascending, then by name ascending
sorted_records = sorted(records, key=lambda r: (r[1], r[0]))
for r in sorted_records:
print(r)- 1–6Four records of (name, age, city).
- 9
key=lambda r: (r[1], r[0])— returns a tuple. Python compares tuples lexicographically: first by age (r[1]), then by name (r[0]) to break ties. - 11–12Output: Bob (25), Dan (25), Ana (30), Cara (30).
sorted(records) sorts by name first, then age, then city. You only need a lambda when you want a different order than left-to-right.🔝 max() and min() with key
max and min accept the same key= argument. The lambda returns the value to compare, not necessarily the value to return.
words = ["apple", "banana", "kiwi", "strawberry"] longest = max(words, key=lambda w: len(w)) shortest = min(words, key=lambda w: len(w)) print(longest) print(shortest)
- 1Four words of varying lengths.
- 3
max(..., key=lambda w: len(w))— the lambda returns each word's length, andmaxpicks the word whose length is largest. Returns"strawberry", not10. - 4Similarly,
minreturns"kiwi"(shortest, length 4).
🗜️ reduce() — Fold a Sequence into One Value
From functools — less common than sum and max, but occasionally the right tool
🗜️ functools.reduce with lambda
reduce(func, iterable) applies func cumulatively from left to right. The first argument is the accumulated result, the second is the next element.
from functools import reduce numbers = [1, 2, 3, 4, 5] product = reduce(lambda acc, x: acc * x, numbers) maximum = reduce(lambda a, b: a if a > b else b, numbers) joined = reduce(lambda a, b: a + "-" + b, ["a", "b", "c"]) print(product) print(maximum) print(joined)
- 1Import
reduce— it lives in the standard library but is not a builtin (unlikemapandfilter). - 3The list to reduce.
- 5
reduce(lambda acc, x: acc * x, [1,2,3,4,5])— computes((((1*2)*3)*4)*5) = 120. The variableaccholds the running total. - 6Maximum via reduce — picks the larger of
aandbeach step. Result:5. - 7String join via reduce. Result:
"a-b-c".
reduce is rarely needed — sum, max, min, math.prod, and "-".join(...) handle the vast majority of folds more readably. Use reduce only when no built-in equivalent exists (e.g. folding with a custom binary operation across more than 100+ elements, where a generator expression would be slower).🔒 Lambdas and Closures
A lambda can capture variables from its enclosing scope — with one notorious gotcha
🔒 Capturing Variables
Lambdas see variables from the surrounding scope at call time, not at definition time. This is usually what you want — but occasionally surprising.
def make_multiplier(n): return lambda x: x * n double = make_multiplier(2) triple = make_multiplier(3) print(double(10)) print(triple(10)) prefix = "Hi, " greet = lambda name: prefix + name print(greet("Ana")) prefix = "Hey, " print(greet("Bob"))
- 1–2
make_multiplierreturns a lambda that capturesn. Each call tomake_multipliercreates a freshnand a fresh lambda that closes over it. - 4–5Two multipliers, each with its own captured
n. - 7–8
double(10)= 20;triple(10)= 30. Capturing works correctly per call. - 10–12
prefixis a module-level name. The lambda looks it up at call time — so it sees the current value ofprefix. - 14–15After changing
prefixto"Hey, ", the samegreetfunction produces a different result:"Hey, Bob".
⚠️ The Classic "Late Binding" Bug
Lambdas created inside a loop all see the same final value of the loop variable. This is the single most common gotcha with lambdas in real code.
# Buggy version — all three lambdas return 2 bad = [lambda: i for i in range(3)] print([f() for f in bad]) # Fixed — bind i as a default argument good = [lambda i=i: i for i in range(3)] print([f() for f in good])
- 2The comprehension creates three lambdas. Each lambda's body is
lambda: i— it looks upifrom the enclosing scope when called, not when created. By the time we call them, the loop has finished andi == 2. - 3All three lambdas return 2:
[2, 2, 2]. Not what we wanted. - 6The fix: use a default argument
i=i. Defaults are evaluated at definition time, so each lambda captures the current value ofiwhen it was created. - 7Now each lambda returns a different value:
[0, 1, 2].
def inside a loop. When you see a lambda capturing a loop variable, check whether it needs the default-argument fix.🚫 Anti-patterns — When NOT to Use a Lambda
Lambdas are useful, but easy to over-use. Here's where they cost more than they save
❌ Assigning a Lambda to a Variable
PEP 8 explicitly discourages this. If you're going to give it a name, use def.
# Discouraged by PEP 8 square = lambda x: x * x # Preferred def square_better(x): return x * x print(square(5)) print(square_better(5))
- 2
square = lambda x: x * x— assigns a lambda to a name. PEP 8 says: if you're giving it a name, write adef. - 5–6The
defversion — same behaviour, but with a proper name, docstring potential, and better tracebacks. - 8–9Both return 25.
❌ Lambdas That Are Too Complex
When a lambda needs nested ternaries, method chains, or a comprehension, it stops being readable. A named function with a docstring is better.
# Too much happening in one line process = lambda items: [i.strip().lower() for i in items if i and len(i) > 2] # Clearer def process_clean(items): return [i.strip().lower() for i in items if i and len(i) > 2] data = [" Hello ", "", "x", " World "] print(process(data)) print(process_clean(data))
- 2The lambda contains a comprehension — technically legal, but hard to read because two logical layers (the lambda, the comprehension) are collapsed onto one line.
- 5–6The
defversion puts the logic on its own line. Same result, far easier to scan. - 8–9Both produce
['hello', 'world'].
❌ Lambda Where a Comprehension Would Do
When you're using map or filter just to apply a simple transform, a comprehension is almost always clearer.
numbers = [1, 2, 3, 4, 5] # Noisy squares_lambda = list(map(lambda x: x * x, numbers)) # Clearer squares_comp = [x * x for x in numbers] print(squares_lambda == squares_comp)
- 1The input list.
- 4
map(lambda x: x * x, numbers)— three layers of nesting (map, lambda, list) to express a simple square. - 7
[x * x for x in numbers]— one layer, reads top-to-bottom. - 9Both produce the same result.
map(lambda ...) pattern is mostly seen in code that came from older functional-style codebases. Modern code prefers comprehensions unless the function is already named (map(str.strip, lines)).🏋️ Practice — Check Your Understanding
Three exercises with solutions. Try each before expanding the answer.
Exercise 1 — Predict the Output
What does each line print?
f = lambda x, y=10: x + y print(f(5)) print(f(5, 20)) ops = [lambda x: x + 1, lambda x: x * 2] print(ops[0](10)) print(ops[1](10))
Show solution
Line 2: 15 — y uses its default of 10.
Line 3: 25 — y is overridden to 20.
Line 6: 11 — the first lambda adds 1.
Line 7: 20 — the second lambda multiplies by 2.
Exercise 2 — Rewrite as a Named Function
Rewrite this lambda as a def function, then explain why you'd prefer one or the other.
celsius = lambda f: (f - 32) * 5 / 9 print(celsius(212)) print(celsius(32))
Show solution
def celsius(f): """Convert Fahrenheit to Celsius.""" return (f - 32) * 5 / 9 print(celsius(212)) print(celsius(32))
In this case the def is better: the function has a meaningful name, it's called multiple times, and a docstring explains what it does. A lambda would only make sense here if celsius were passed inline as an argument to something else — e.g. sorted(fahrenheit_temps, key=celsius).
Exercise 3 — Fix the Late-Binding Bug
This code is intended to create a list of functions that return 0, 1, 2 — but it returns 2, 2, 2. Fix it.
funcs = [lambda: i for i in range(3)] print([f() for f in funcs])
Show solution
funcs = [lambda i=i: i for i in range(3)] print([f() for f in funcs])
The lambdas all close over the same i, which is 2 when the loop ends. The fix binds the current value of i as a default argument i=i at definition time, so each lambda captures a different value. Result: [0, 1, 2].
❓ Frequently Asked Questions
Common questions about lambdas, answered directly
A lambda is an anonymous, single-expression function created with the lambda keyword. Unlike def, it has no name, no docstring, and no statements — just a list of parameters followed by one expression that is returned automatically. Lambdas are typically used where a function is needed briefly and naming it would add noise (e.g. as the key argument to sorted).
Use a lambda when the function is trivial (one expression), used once, and passing a named def would require writing three lines for something the reader can absorb instantly. Use def when the function has a name that conveys meaning, has more than one statement, needs a docstring, or is used in more than one place. In real code, most lambdas are the key= argument of sorted or the body of a callback — nowhere else.
No. A lambda is a function object just like one defined with def — same call overhead, same closure behaviour. The only difference is syntax. The decision between lambda and def should always be about readability, never about speed.
No. A lambda body must be a single expression. If you need if/else logic, use a ternary expression: lambda x: 'pos' if x > 0 else 'neg'. If you need multiple statements, a loop, or exception handling, use def. The single-expression rule is intentional — it keeps lambdas short and readable.
They produce equivalent results, but list comprehensions are usually more readable in Python. map(lambda x: x*2, nums) is less clear than [x*2 for x in nums], and filter(lambda x: x>0, nums) is less clear than [x for x in nums if x>0]. The main reason to use map/filter today is when you already have a named function — map(str.strip, lines) is arguably cleaner than [line.strip() for line in lines].
Because lambdas close over variable names, not values. If you create lambdas in a loop like [lambda: i for i in range(3)], every lambda sees the final value of i (2) because that's the value bound when they are called — not when they were created. Fix it by binding the current value as a default argument: [lambda i=i: i for i in range(3)].
📋 Quick-Reference Table
Every common lambda pattern at a glance
| Pattern | Example | Notes |
|---|---|---|
| Basic | lambda x: x + 1 | Single expression, auto-return |
| Two arguments | lambda a, b: a + b | Like a def with two parameters |
| No arguments | lambda: 42 | Valid but rarely useful |
| Default | lambda x, y=10: x + y | Defaults evaluated at definition |
| *args | lambda *nums: sum(nums) | Collects positional arguments |
| **kwargs | lambda **opts: opts.get("k") | Collects keyword arguments |
| Ternary | lambda x: "pos" if x > 0 else "neg" | Conditional expression — not an if/else statement |
| sorted key | sorted(items, key=lambda x: x[1]) | The most idiomatic use |
| Multi-field sort | sorted(items, key=lambda r: (r[1], r[0])) | Tuple key for multiple fields |
| max/min key | max(words, key=lambda w: len(w)) | Returns the item, not the key |
| map | list(map(lambda x: x*2, nums)) | Comprehension usually clearer |
| filter | list(filter(lambda x: x > 0, nums)) | Comprehension usually clearer |
| filter(None) | list(filter(None, mixed)) | Keeps truthy items, no lambda needed |
| reduce | reduce(lambda a, b: a + b, nums) | Needs from functools import reduce |
| Closure | lambda x, n=n: x * n | Bind loop variables as defaults |
| Anti-pattern | square = lambda x: x*x | Prefer def square(x): return x*x |
Educational Use Disclaimer
This page is part of Poetic Bytes, a free educational resource for learning Python programming. The code examples are provided for instruction and demonstration. They are not production-audited and should not be used as-is in security-sensitive, financial, or safety-critical systems without independent review. See our full disclaimer and terms of use.
📬 Python, weekly
Short lessons, real code, and ecosystem news — sent about once a week
🗣️ Learner Voices
No comments yet — your voice starts the conversation.