Python 3.12+ · Updated November 2026 · Every program explained line by line

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)
Written & reviewed by Glenn Junsay Pansensoy Last reviewed: November 2026 Python 3.12+ ~25 min read

🎯 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.

💡 In modern Python (3.10+), you'll see lambdas used in three main places: the 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__)
Line-by-line explanation
  1. 1lambda x: x * 2 — takes one parameter x, returns x * 2. The result is a function object, bound to double.
  2. 2Two parameters: a, b. Returns their sum.
  3. 3An f-string as the body works — it's a single expression.
  4. 4Zero parameters: lambda: is valid, returning a fixed value.
  5. 6–9Calling each lambda works exactly like calling a def function. Results: 14, 7, "Hello, Ana!", 42.
  6. 10The result of type(double).__name__ is "function" — a lambda produces a real function object, not a special type.
Expected output14 7 Hello, Ana! 42 function

🎯 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))
Line-by-line explanation
  1. 1Chained ternary (ternaries can nest, though readability suffers past one level). Returns "pos", "zero", or "neg".
  2. 2Conditional expression: divide if b is truthy, else return None. Guards against ZeroDivisionError.
  3. 3Single expression using built-in max and min to clamp.
  4. 5–10Verify the behaviour: "pos", "neg", "zero", 5.0, None, 10.
💡 What you cannot put in a lambda: 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="!!!"))
Line-by-line explanation
  1. 1exp=2 is a default parameter. Calling power(3) squares 3; calling power(3, 4) raises it to the 4th power.
  2. 2*nums collects all positional arguments into a tuple.
  3. 3**opts collects keyword arguments. opts.get("suffix", "!") returns a default if not provided.
  4. 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
NameAnonymous (unless assigned)Named
Number of statementsExactly one expressionAny number
Return statementImplicit (expression is returned)Explicit return
DocstringNot possiblePossible
Type hintsNot on parameters, not on returnFully supported
TracebacksShow <lambda> as the function nameShow the real function name
DebuggingHard to break intoEasy — set a breakpoint in the body
Best forInline 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))
Line-by-line explanation
  1. 2–3The def version: named, with an explicit return. Two lines of code, one name.
  2. 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 — def would be clearer here.
  3. 8–9Both print 25.
💡 PEP 8 discourages assigning a lambda to a variable. If you write 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)
Line-by-line explanation
  1. 1Input list.
  2. 3–5Three uses of map: square each, double each, add a # prefix. Each map call returns an iterator; list(...) materialises it.
  3. 7–9Print the three resulting lists.
  4. 12–13When a named function does the job — str.strip is already a function — pass it directly. No lambda needed. Result: ['ana', 'bob', 'cara'].
💡 When you already have a named function (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)
Line-by-line explanation
  1. 1Input list.
  2. 3Keeps even numbers: [2, 4, 6].
  3. 4Keeps numbers greater than 3: [4, 5, 6].
  4. 9–11filter(None, iterable) — special case: uses Python's truthiness rules instead of calling a predicate. Keeps 1, "hi", 42. Drops 0, None, "".
💡 For the same operation, comprehensions are usually clearer: [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"])
Line-by-line explanation
  1. 1–5Three student dicts.
  2. 7key=lambda s: s["score"] — sorted calls the lambda on each student and compares the results. Sorts ascending by score.
  3. 8reverse=True sorts descending — highest score first.
  4. 9Sorting by name — a different key on the same data.
  5. 11–12Print in descending-score order: Ana (95), Cara (92), Bob (88).
  6. 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)
Line-by-line explanation
  1. 1–6Four records of (name, age, city).
  2. 9key=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.
  3. 11–12Output: Bob (25), Dan (25), Ana (30), Cara (30).
💡 For tuples specifically, Python's default sorting is already lexicographic — 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)
Line-by-line explanation
  1. 1Four words of varying lengths.
  2. 3max(..., key=lambda w: len(w)) — the lambda returns each word's length, and max picks the word whose length is largest. Returns "strawberry", not 10.
  3. 4Similarly, min returns "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)
Line-by-line explanation
  1. 1Import reduce — it lives in the standard library but is not a builtin (unlike map and filter).
  2. 3The list to reduce.
  3. 5reduce(lambda acc, x: acc * x, [1,2,3,4,5]) — computes ((((1*2)*3)*4)*5) = 120. The variable acc holds the running total.
  4. 6Maximum via reduce — picks the larger of a and b each step. Result: 5.
  5. 7String join via reduce. Result: "a-b-c".
💡 In practice, 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"))
Line-by-line explanation
  1. 1–2make_multiplier returns a lambda that captures n. Each call to make_multiplier creates a fresh n and a fresh lambda that closes over it.
  2. 4–5Two multipliers, each with its own captured n.
  3. 7–8double(10) = 20; triple(10) = 30. Capturing works correctly per call.
  4. 10–12prefix is a module-level name. The lambda looks it up at call time — so it sees the current value of prefix.
  5. 14–15After changing prefix to "Hey, ", the same greet function produces a different result: "Hey, Bob".
Expected output20 30 Hi, Ana 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])
Line-by-line explanation
  1. 2The comprehension creates three lambdas. Each lambda's body is lambda: i — it looks up i from the enclosing scope when called, not when created. By the time we call them, the loop has finished and i == 2.
  2. 3All three lambdas return 2: [2, 2, 2]. Not what we wanted.
  3. 6The fix: use a default argument i=i. Defaults are evaluated at definition time, so each lambda captures the current value of i when it was created.
  4. 7Now each lambda returns a different value: [0, 1, 2].
💡 This is a general Python closure rule, not a lambda-specific one — the same bug appears with 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))
Line-by-line explanation
  1. 2square = lambda x: x * x — assigns a lambda to a name. PEP 8 says: if you're giving it a name, write a def.
  2. 5–6The def version — same behaviour, but with a proper name, docstring potential, and better tracebacks.
  3. 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))
Line-by-line explanation
  1. 2The lambda contains a comprehension — technically legal, but hard to read because two logical layers (the lambda, the comprehension) are collapsed onto one line.
  2. 5–6The def version puts the logic on its own line. Same result, far easier to scan.
  3. 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)
Line-by-line explanation
  1. 1The input list.
  2. 4map(lambda x: x * x, numbers) — three layers of nesting (map, lambda, list) to express a simple square.
  3. 7[x * x for x in numbers] — one layer, reads top-to-bottom.
  4. 9Both produce the same result.
💡 In 2026 Python, the 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
Basiclambda x: x + 1Single expression, auto-return
Two argumentslambda a, b: a + bLike a def with two parameters
No argumentslambda: 42Valid but rarely useful
Defaultlambda x, y=10: x + yDefaults evaluated at definition
*argslambda *nums: sum(nums)Collects positional arguments
**kwargslambda **opts: opts.get("k")Collects keyword arguments
Ternarylambda x: "pos" if x > 0 else "neg"Conditional expression — not an if/else statement
sorted keysorted(items, key=lambda x: x[1])The most idiomatic use
Multi-field sortsorted(items, key=lambda r: (r[1], r[0]))Tuple key for multiple fields
max/min keymax(words, key=lambda w: len(w))Returns the item, not the key
maplist(map(lambda x: x*2, nums))Comprehension usually clearer
filterlist(filter(lambda x: x > 0, nums))Comprehension usually clearer
filter(None)list(filter(None, mixed))Keeps truthy items, no lambda needed
reducereduce(lambda a, b: a + b, nums)Needs from functools import reduce
Closurelambda x, n=n: x * nBind loop variables as defaults
Anti-patternsquare = lambda x: x*xPrefer def square(x): return x*x
Review comprehensions

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.

🗣️ Learner Voices

No comments yet — your voice starts the conversation.