Lambda Expressions

A lambda is a small, unnamed function reduced to a single expression — perfect as a throwaway key function, but no substitute for a real def when logic gets complex.

A lambda expression creates an anonymous function inline, restricted to a single expression whose value is automatically returned. Lambdas exist for exactly one situation: a short, one-off function needed as a value — most often as the key argument to sorted(), max(), or similar higher-order functions. They are not a general replacement for def, and Python deliberately keeps them limited to discourage cramming complex logic into a single unreadable line.

Lambda Syntax

The general form is lambda parameters: expression. There's no def, no name (unless you assign it to one), and no return keyword — the expression's value is returned automatically.

square = lambda x: x * x
print(square(5))   # 25

# Equivalent def form:
def square(x):
    return x * x

Expression Body Only

A lambda's body must be a single expression — it cannot contain statements like if/else blocks (though a conditional *expression* is fine), loops, multiple lines, or assignments (except via the walrus operator in specific contexts).

# A conditional EXPRESSION is fine (ternary form):
abs_val = lambda x: x if x >= 0 else -x
print(abs_val(-7))   # 7

# But a for-loop, print statement, or multi-line block is NOT allowed inside a lambda:
# broken = lambda x: for i in range(x): print(i)   # SyntaxError

Parameters

Lambdas support the same parameter features as regular functions: positional, keyword, defaults, *args, and **kwargs — just without the def line's name and parentheses-around-the-whole-list punctuation.

add = lambda a, b=10: a + b
print(add(5))          # 15
print(add(5, 20))      # 25

sum_all = lambda *nums: sum(nums)
print(sum_all(1, 2, 3))  # 6

Use Cases

The overwhelmingly common use of lambda is as a short key= function passed to sorting or aggregation built-ins, where defining a separate named function would be overkill for a one-liner.

people = [("Riya", 29), ("Sam", 22), ("Amara", 35)]

by_age = sorted(people, key=lambda person: person[1])
print(by_age)   # [('Sam', 22), ('Riya', 29), ('Amara', 35)]

youngest = min(people, key=lambda person: person[1])
print(youngest)  # ('Sam', 22)

Sorting and Key Functions

sorted(), list.sort(), min(), and max() all accept a key callable that maps each element to the value used for comparison. Lambdas are the idiomatic way to supply a quick projection without writing a full def.

words = ["banana", "kiwi", "apple", "fig"]

by_length = sorted(words, key=lambda w: len(w))
print(by_length)   # ['fig', 'kiwi', 'apple', 'banana']

by_length_desc = sorted(words, key=lambda w: len(w), reverse=True)
print(by_length_desc)  # ['banana', 'apple', 'kiwi', 'fig']

# operator.itemgetter/attrgetter are the standard-library alternative for simple cases
from operator import itemgetter
by_age2 = sorted(people, key=itemgetter(1))

Limitations

Lambdas cannot contain statements (assignments other than walrus, if/elif/else blocks, loops, try/except), cannot have docstrings, and cannot span multiple logical statements. They also have no __name__ beyond the generic '<lambda>', which hurts debugging/traceback readability compared to a named def.

PEP 8 explicitly recommends against assigning a lambda to a name (f = lambda x: x + 1) — use def f(x): return x + 1 instead, since a def gives the function a real, traceback-friendly name for no extra cost.

f = lambda x: x + 1
print(f.__name__)   # '<lambda>' — unhelpful in a traceback compared to a real function name

Closures in Lambdas

Lambdas form closures exactly like nested def functions do — they capture variables from their enclosing scope by reference, not by value at creation time. This is the same late-binding behavior that trips people up in def-based closures too.

multipliers = [lambda x: x * i for i in range(3)]
print([m(10) for m in multipliers])
# [20, 20, 20] — NOT [0, 10, 20]! All three lambdas share the same 'i',
# which is 2 by the time the loop finishes.

# Fix: capture the current value via a default argument
multipliers_fixed = [lambda x, i=i: x * i for i in range(3)]
print([m(10) for m in multipliers_fixed])
# [0, 10, 20] — i=i binds the CURRENT value at lambda-creation time

Practical Usage: When to Reach for def Instead

Use a lambda when the logic is a single short expression used immediately, typically inline as an argument. Once the logic needs a name for reuse, a docstring, multiple statements, or error handling, switch to a proper def — it's not slower, and it's dramatically more readable and debuggable.

# Good use of lambda — short, inline, throwaway:
names = sorted(["Zara", "amir", "Beth"], key=lambda s: s.lower())

# Bad use — this deserves a real function:
validate = lambda x: True if x > 0 and x < 100 and x % 2 == 0 else False
# Better:
def is_valid_even_percentage(x):
    """True if x is an even number strictly between 0 and 100."""
    return 0 < x < 100 and x % 2 == 0

Mental Model

Think of lambda args: expr as syntactic sugar for a minimal, unnamed def: def <lambda>(args): return expr. Everything you know about scoping, closures, and default arguments for regular functions applies identically — lambda changes only the syntax, not the underlying function semantics.

Quick Reference

lambda: 42                     # no parameters
lambda x: x + 1                # one parameter
lambda x, y=0: x + y           # default parameter
lambda *args, **kwargs: args   # variadic
lambda x: x if x > 0 else -x   # conditional expression only, no if/else statement

Key Takeaways

A lambda is an anonymous function limited to a single expression, whose value is returned automatically.

Lambdas support the same parameter forms as def — positional, defaults, *args, **kwargs.

The dominant real use is as a key= function for sorted(), min(), max(), and similar higher-order functions.

Lambdas cannot contain statements (loops, multi-line blocks, try/except) — only a single expression.

Lambdas close over enclosing variables by reference, just like nested def functions — including the late-binding loop-variable trap.

PEP 8 discourages assigning a lambda to a name; use def instead once a function needs a real, debuggable identity.

callbacks = [lambda: print(i) for i in range(3)] for cb in callbacks: cb() # prints 2, 2, 2 callbacks = [lambda i=i: print(i) for i in range(3)] for cb in callbacks: cb() # prints 0, 1, 2

Lambdas capture the loop variable 'i' by reference, so by the time the callbacks run, every lambda sees the loop's final value; binding it as a default argument (i=i) captures the value at creation time instead.

add_one = lambda x: x + 1 def add_one(x): return x + 1 PEP 8 recommends against binding a lambda to a name — a def gives the function a real, traceback-friendly __name__ with no downside, so there's no reason to use lambda here.

process = lambda x: (print(x), x * 2)[1] # abusing tuple indexing to fake multiple statements def process(x): print(x) return x * 2

Lambdas cannot contain multiple statements; contorting a tuple-and-index trick to smuggle in a side effect is unreadable — this logic belongs in a real function.

What is the general syntax of a lambda expression?

lambda parameters: expression — it takes zero or more parameters (same forms as def) followed by a colon and a single expression whose value is automatically returned.

Why can't a lambda contain an if/else statement block or a for loop?

Because a lambda's body is restricted to a single expression, not a sequence of statements; Python's grammar only allows expression-level constructs (like a conditional expression x if cond else y) inside a lambda, not statement-level constructs like if blocks or loops.

What is the single most common practical use case for lambda in everyday Python code?

Supplying a short, throwaway key= function to sorted(), min(), max(), or similar higher-order functions, to project each element to the value used for comparison.

Why does a list of lambdas created in a loop often produce the same result when called later, and how do you fix it?

Because lambdas capture the loop variable by reference (late binding), so by the time they're called, the loop variable holds its final value for every lambda; the fix is to bind the current value as a default argument, e.g. lambda x, i=i: ....

What does PEP 8 recommend regarding assigning a lambda to a variable name, and why?

It recommends using a regular def statement instead, because a def gives the function a proper name (shown in tracebacks and introspection) while a lambda's __name__ is always the unhelpful '<lambda>'.

Can a lambda accept *args and **kwargs like a regular function?

Yes — lambda *args, **kwargs: ... works exactly the same way it would in a def, since lambda supports all the same parameter forms.

How would you describe the relationship between lambda and def in terms of underlying function semantics?

They are functionally equivalent under the hood — a lambda is essentially syntactic sugar for a minimal, anonymous def whose body is a single return statement; scoping, closures, and defaults behave identically for both.

Write code that sorts a list of dictionaries [{'name': 'Sam', 'score': 82}, ...] by 'score' descending using a lambda key.

students = [ {"name": "Sam", "score": 82}, {"name": "Amara", "score": 95}, {"name": "Riya", "score": 78}, ] ranked = sorted(students, key=lambda s: s["score"], reverse=True) print([s["name"] for s in ranked])

['Amara', 'Sam', 'Riya'] — sorted by score descending using the lambda projection s['score'].

What does this print?

[2, 2, 2] — all three lambdas share the same enclosing variable 'i' by reference, and by the time they're called, the loop has finished with i == 2.

Write a single line using filter() and a lambda to get all even numbers from a list [1..10].

numbers = list(range(1, 11)) evens = list(filter(lambda n: n % 2 == 0, numbers)) print(evens)

[2, 4, 6, 8, 10] — filter() keeps only the elements where the lambda returns a truthy value.

15 then 6 — the lambda behaves exactly like a def with a default parameter: f(3) uses y=5 (3*5=15), f(3, 2) overrides it (3*2=6).

funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])
f = lambda x, y=5: x * y
print(f(3))
print(f(3, 2))