7.2 Lambda Expressions and Higher-Order Functions
Key Takeaways
- The lambda keyword defines anonymous, inline function objects evaluated dynamically at runtime using the syntax lambda arguments: expression.
- Lambda bodies are restricted to a single syntactic expression; statements (return, raise, pass, assignments, loops) cannot be placed inside a lambda.
- map(func, *iterables) and filter(func, iterable) return lazy iterators in Python 3 that compute transformed or filtered items on demand.
- filter(None, iterable) removes all falsy values (0, False, empty strings, None, empty collections) using Python's native truthiness evaluation.
- sorted(), list.sort(), min(), and max() accept a unary key callable parameter to perform custom, complex, and multi-criteria comparisons.
Lambda Expressions and Higher-Order Functions
In Python, functions are first-class citizens—they can be assigned to variables, passed as arguments to other functions, stored in data structures, and returned as values from functions. A function that accepts another function as an argument or returns a function is called a higher-order function. To support functional paradigms without cluttering namespaces with trivial helper functions, Python provides anonymous functions via the lambda keyword.
1. Anonymous Functions and the lambda Keyword
A lambda function (or lambda form) is an inline, un-named function defined at the point of expression evaluation.
Syntax
lambda parameter1, parameter2, ...: expression
# Standard named function
def square(x):
return x ** 2
# Equivalent anonymous lambda function assigned to a variable
sq = lambda x: x ** 2
print(square(5)) # 25
print(sq(5)) # 25
print(type(sq)) # <class 'function'>
At runtime, Python treats a function created via def and a function created via lambda as instances of the exact same class: types.FunctionType. The only significant difference is that lambda functions have their __name__ attribute set to "<lambda>" instead of an explicit identifier.
2. Argument Support and Syntactic Restrictions
While lambdas can accept any standard Python argument pattern, their internal structure is strictly limited by the grammar of the language.
Argument Varieties
Lambda expressions support all parameter passing mechanisms available to standard functions:
# No arguments
get_pi = lambda: 3.14159
print(get_pi()) # 3.14159
# Multiple positional arguments
add = lambda x, y: x + y
print(add(10, 20)) # 30
# Default parameter values
greet = lambda name, prefix="Hello": f"{prefix}, {name}!"
print(greet("Alice")) # Hello, Alice!
print(greet("Bob", prefix="Hi")) # Hi, Bob!
# Variable positional arguments (*args)
sum_all = lambda *args: sum(args)
print(sum_all(1, 2, 3, 4, 5)) # 15
# Keyword arguments (**kwargs)
format_kv = lambda **kwargs: ", ".join(f"{k}={v}" for k, v in kwargs.items())
print(format_kv(host="localhost", port=8080)) # host=localhost, port=8080
Rigid Syntactic Restrictions on the Lambda Body
On the PCAP exam, understanding what cannot be written inside a lambda is just as important as knowing what can:
- Single Expression Only: The body of a lambda must contain exactly one syntactic expression. It cannot contain a suite of multiple expressions separated by semicolons or newlines.
- Implicit Return: The result of evaluating the single expression is automatically returned. Writing an explicit
returnkeyword inside a lambda raises aSyntaxError. - No Statements Allowed: Statements perform actions but do not evaluate to values. The following statements are strictly forbidden inside a lambda:
- Assignments:
lambda x: x = x + 1(SyntaxError) - Control flow statements:
while,for,pass,break,continue(SyntaxError) - Exception statements:
raise ValueError(),try,except(SyntaxError) - Assertions:
assert x > 0(SyntaxError)
- Assignments:
- Conditional Expressions ARE Permitted: While an
ifstatement is forbidden, Python's ternary conditional expression (expr1 if cond else expr2) evaluates to a single value and is fully legal:
# Legal: Ternary expression in lambda
parity = lambda n: "Even" if n % 2 == 0 else "Odd"
print(parity(7)) # Odd
print(parity(8)) # Even
3. The map() Built-in Function
The built-in map() function applies a transformation function to every item of an iterable and yields the results.
Signature and Laziness
map(function, iterable, *additional_iterables)
In Python 3, map() returns a lazy map iterator object (<class 'map'>), computing values only when requested rather than building an entire list in memory:
numbers = [1, 2, 3, 4, 5]
mapped_iter = map(lambda x: x * 10, numbers)
print(mapped_iter) # <map object at 0x...>
print(next(mapped_iter)) # 10
print(next(mapped_iter)) # 20
# Convert remaining elements to a concrete list
print(list(mapped_iter)) # [30, 40, 50]
Multiple Iterables and the Shortest Termination Rule
If multiple iterables are supplied to map(), the mapping function must accept as many arguments as there are iterables. Iteration terminates as soon as the shortest iterable is exhausted:
list_a = [1, 2, 3, 4, 5]
list_b = [10, 20, 30] # Length 3 (shortest)
sums = list(map(lambda x, y: x + y, list_a, list_b))
print(sums) # [11, 22, 33] (Elements 4 and 5 of list_a are ignored)
4. The filter() Built-in Function
The built-in filter() function constructs an iterator from elements of an iterable for which a predicate function returns a truthy value.
Signature
filter(function_or_None, iterable)
values = [12, -4, 0, 25, -9, 30, -1]
positives = list(filter(lambda x: x > 0, values))
print(positives) # [12, 25, 30]
The None Predicate Feature (Truthiness Filter)
If the first argument to filter() is None, Python uses the identity truth-testing function. It filters out all elements that evaluate to False in a boolean context (0, 0.0, "", [], {}, None, False):
mixed_data = [0, "hello", False, 42, "", None, [1, 2], {}, -5]
truthy_items = list(filter(None, mixed_data))
print(truthy_items) # ['hello', 42, [1, 2], -5]
Comparing map() / filter() with Comprehensions
| Pattern | Higher-Order Function Pipeline | Equivalent List Comprehension |
| :--- | :--- | :--- | :--- |
| Transform | list(map(lambda x: x**2, seq)) | [x**2 for x in seq] |
| Filter | list(filter(lambda x: x > 0, seq)) | [x for x in seq if x > 0] |
| Filter + Transform | list(map(lambda x: x**2, filter(lambda x: x > 0, seq))) | [x**2 for x in seq if x > 0] |
In modern Python, list comprehensions are generally preferred for readability, but map() and filter() remain essential when working with existing named callables (e.g., map(int, string_list) or filter(str.isalpha, chars)).
5. Writing Your Own Functions That Accept Lambdas
map() and filter() are not special: they simply declare a parameter and call it. Objective
PCAP-31-03 5.2 explicitly covers self-defined functions taking lambdas as arguments, and
writing one requires no syntax you have not already seen — the parameter is an ordinary name that
happens to hold a callable.
def apply_twice(operation, value):
"""Call `operation` on `value`, then on the result."""
return operation(operation(value))
print(apply_twice(lambda x: x * 3, 2)) # 18 -> (2*3)*3
print(apply_twice(lambda s: s + "!", "hi")) # 'hi!!'
A second common shape is a self-defined function that builds a result under a caller-supplied
rule, which is how key= parameters work internally:
def summarize(items, transform, keep):
result = []
for item in items:
if keep(item):
result.append(transform(item))
return result
readings = [-4, 7, 0, 12, -1]
print(summarize(readings, lambda n: n * 100, lambda n: n > 0))
# [700, 1200]
Two rules govern these designs:
- A lambda is just a
functionobject.type(lambda x: x)is<class 'function'>, so a parameter holding a lambda can equally receive adef-defined function, a built-in such asabs, or a method reference such asstr.upper. - Give the callable parameter a default when the operation is optional.
def scale(seq, op=lambda x: x):lets callers omit the transformation entirely.
Exam Trap:
apply_twice(lambda x: x * 3, 2)passes the lambda object; writingapply_twice(lambda x: x * 3(2))instead calls3(2)inside the lambda body and raisesTypeError: 'int' object is not callablethe moment the lambda runs.
6. Custom Sorting with sorted(), list.sort(), min(), and max()
Python provides powerful sorting mechanisms through the built-in sorted() function and the mutable list.sort() method.
sorted() vs list.sort()
sorted(iterable, key=None, reverse=False): Returns a new sorted list from any iterable; leaves the original iterable untouched.list.sort(key=None, reverse=False): Modifies the list in-place and returnsNone.
The key Parameter and Lambdas
The key parameter accepts a unary function (a function taking one argument) that extracts or computes a comparison key for each element. The elements are sorted based on the return values of this key function.
# Sort strings by length
words = ["elephant", "cat", "hippopotamus", "dog", "dolphin"]
sorted_by_len = sorted(words, key=lambda w: len(w))
print(sorted_by_len) # ['cat', 'dog', 'elephant', 'dolphin', 'hippopotamus']
# Sort list of tuples by second element (e.g. price)
items = [("Laptop", 1200), ("Mouse", 25), ("Monitor", 300), ("Keyboard", 75)]
sorted_by_price = sorted(items, key=lambda item: item[1])
print(sorted_by_price)
# [('Mouse', 25), ('Keyboard', 75), ('Monitor', 300), ('Laptop', 1200)]
Multi-Criteria Sorting via Tuples
When sorting requires multiple precedence rules (e.g. sort primarily by score descending, secondarily by student name ascending), the lambda can return a tuple:
students = [
{"name": "Charlie", "score": 88},
{"name": "Alice", "score": 95},
{"name": "Bob", "score": 88},
{"name": "David", "score": 95}
]
# Primary sort: score descending (-score), Secondary sort: name ascending (name)
students_sorted = sorted(students, key=lambda s: (-s["score"], s["name"]))
print(students_sorted)
# [
# {'name': 'Alice', 'score': 95},
# {'name': 'David', 'score': 95},
# {'name': 'Bob', 'score': 88},
# {'name': 'Charlie', 'score': 88}
# ]
Custom Key with min() and max()
The min() and max() built-ins also accept the key parameter to identify extreme elements based on custom criteria:
employees = [
{"name": "Sarah", "salary": 95000},
{"name": "John", "salary": 120000},
{"name": "Emma", "salary": 82000}
]
top_earner = max(employees, key=lambda emp: emp["salary"])
lowest_earner = min(employees, key=lambda emp: emp["salary"])
print("Top Earner:", top_earner["name"]) # Top Earner: John
print("Lowest Earner:", lowest_earner["name"]) # Lowest Earner: Emma
What is the output of the following Python program?
nums1 = [1, 2, 3, 4]
nums2 = [10, 20]
result = list(map(lambda a, b: a + b, nums1, nums2))
print(result)
What is the output of passing the following list to filter() with None as the predicate?
data = [0, 1, False, 2, '', 'Python', None, [], [99]]
result = list(filter(None, data))
print(result)
Which of the following lambda expressions will raise a SyntaxError during script compilation?
Consider the following Python code that sorts a list of tuples:
What is the value of sorted_records?records = [('Alice', 85), ('Bob', 90), ('Charlie', 85), ('David', 90)]
sorted_records = sorted(records, key=lambda r: (-r[1], r[0]))
print(sorted_records)