Lambdas and comprehensions
These three features have something in common: they let you express ideas that would otherwise take several lines in a single, readable expression. Used well, they make code shorter and clearer. Used badly, they make it unreadable. This chapter covers when to reach for each one and when to stop.
Lambda functions
A lambda is a nameless, one-expression function. You create it with the lambda keyword. Its real usefulness is that you can write it inline, right where you need it, without defining a named function first. This is what makes it useful with sorted().
double = lambda x: x * 2
double(5) # 10That is equivalent to:
def double(x):
return x * 2For most cases, use def. Lambdas have one real advantage: you can write them inline, right where you need them, without naming them. This is what makes them useful with sorted(), map(), and filter():
players = [("Alice", 87), ("Bob", 74), ("Carol", 92)]
sorted(players, key=lambda p: p[1]) # sort by score (ascending)
sorted(players, key=lambda p: p[1], reverse=True) # sort by score (descending)Without a lambda, you would have to define a named function just for the key= argument. The lambda keeps the intent local and visible.
Lambdas can take multiple arguments:
add = lambda a, b: a + b
add(3, 4) # 7When to use a lambda: only when it is a simple expression used in one place. If it is getting complex, or you need to reuse it, write a proper def. A lambda that spans several operators or requires conditionals is usually a sign to switch to def.
List comprehensions
The most common transformation in Python: take a sequence, do something to each item, get a new list back. A list comprehension does this in one readable line: [expression for item in iterable]. You can also add a filter with if.
The long way:
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
squares.append(n ** 2)The list comprehension:
squares = [n ** 2 for n in numbers]The structure is always the same: [expression for item in iterable].
scores = [87, 42, 96, 55, 71]
scaled = [s * 1.1 for s in scores] # apply a 10% bonus
as_grades = [f"{s}/100" for s in scores] # format each oneFiltering with a condition
Add an if clause to include only items that pass a test. The result is a new list with only the items where the condition is True.
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
evens = [n for n in numbers if n % 2 == 0] # [2, 4, 6, 8]
odds = [n for n in numbers if n % 2 != 0] # [1, 3, 5, 7]scores = [87, 42, 96, 55, 71, 38]
passing = [s for s in scores if s >= 60] # [87, 96, 71]
failing = [s for s in scores if s < 60] # [42, 55, 38]Nested comprehensions
You can nest comprehensions to flatten a list of lists into a single list. Read it left to right: for each row, for each item in that row, include the item.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [item for row in matrix for item in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]Read it left to right: for each row in matrix, for each item in row, include item.
Nested comprehensions can get confusing fast. If it takes more than a moment to parse, write the loops explicitly.
Dict comprehensions
Dict comprehensions build a dictionary in one expression, using the same idea as list comprehensions: {key: value for item in iterable}. Add a filter with if just like with list comprehensions.
names = ["alice", "bob", "carol"]
scores = [87, 74, 92]
score_map = {name: score for name, score in zip(names, scores)}
# {"alice": 87, "bob": 74, "carol": 92}With a filter:
passing = {name: score for name, score in score_map.items() if score >= 80}
# {"alice": 87, "carol": 92}words = ["apple", "banana", "cherry"]
word_lens = {word: len(word) for word in words}
# {"apple": 5, "banana": 6, "cherry": 6}Set comprehensions
Set comprehensions build a set in one expression, with curly braces and no colon. Because the result is a set, duplicates are automatically removed.
words = ["apple", "banana", "cherry", "apple"]
unique = {w.lower() for w in words} # {"apple", "banana", "cherry"}Use set comprehensions when you want unique values and do not care about order.
Generator expressions
Generators look like list comprehensions with parentheses instead of brackets. The key difference: a list comprehension builds the entire list in memory at once. A generator produces values one at a time, only when needed. For large sequences, this uses far less memory.
squares_gen = (n ** 2 for n in range(1000000))total = sum(n ** 2 for n in range(1000000)) # sum() consumes the generatorWhen passing a generator directly to a function like sum(), max(), min(), or any(), you can drop the extra parentheses:
total = sum(n ** 2 for n in range(1000)) # one set of parens, not twoFor most everyday code, list comprehensions are fine. Use generators when you are processing large datasets or streaming data where holding everything in memory would be wasteful.
zip()
zip() pairs items from two or more sequences together so you can loop through them in parallel. It stops at the shortest sequence. It is the clean way to avoid managing indexes when two lists correspond to each other.
names = ["Alice", "Bob", "Carol"]
scores = [87, 74, 92]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Alice: 87
# Bob: 74
# Carol: 92zip() stops at the shortest sequence. If your sequences might be different lengths, use itertools.zip_longest() with a fill value.
To convert back from a zipped list of pairs into two separate lists, use zip(*pairs):
pairs = [("Alice", 87), ("Bob", 74), ("Carol", 92)]
names, scores = zip(*pairs)
# names = ("Alice", "Bob", "Carol")
# scores = (87, 74, 92)What does * do here?
*pairs unpacks the list into separate arguments: zip(*pairs) becomes zip(("Alice", 87), ("Bob", 74), ("Carol", 92)). The * operator is covered in the Functions chapter.
zip() is also the clean way to iterate multiple sequences in parallel without managing indexes manually:
before = [10, 20, 30]
after = [15, 18, 35]
for b, a in zip(before, after):
change = a - b
print(f"{b} -> {a} ({'+' if change >= 0 else ''}{change})")map() and filter()
map() and filter() are older functional-style tools that do what comprehensions do. You will see them in older code, so it is worth knowing what they mean. Prefer comprehensions for new code; they are more readable to most Python developers.
numbers = [1, 2, 3, 4, 5]
list(map(lambda x: x ** 2, numbers)) # [1, 4, 9, 16, 25]
list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]Prefer comprehensions; they are more readable to most Python developers. Use map() when you have a named function that already exists:
strings = ["1", "2", "3"]
numbers = list(map(int, strings)) # [1, 2, 3] (cleaner than a comprehension here)In practice
Filter a player list to passing scores, rank by score with sorted and a lambda, then print with enumerated positions:
players = [
{"name": "Alice", "score": 87},
{"name": "Bob", "score": 42},
{"name": "Carol", "score": 96},
{"name": "Dave", "score": 55},
]
passing = [p for p in players if p["score"] >= 60]
ranked = sorted(passing, key=lambda p: p["score"], reverse=True)
score_map = {p["name"]: p["score"] for p in ranked}
for i, (name, score) in enumerate(score_map.items(), start=1):
print(f"{i}. {name}: {score}")
