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 only 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 small expression used in one place. If it is growing, or you need to reuse it, write a proper def. A lambda that spans several operators or needs a conditional is usually a sign to switch to def.
lambda keyword. Its whole reason to exist is going inline as a key= for sorted() so you don't define a separate function for one job. The moment it gets longer than one neat expression, I write a real def and feel better for it. 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 one[expression for item in iterable] takes a sequence, does one thing to each item, and hands you back a new list. Read it left to right and it says exactly what it does. This was the first Python feature that made me feel like I was writing Python rather than translating from another language. Filtering 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]if condition at the end to keep only the items that pass the test: [x for x in data if x > 0]. Anything that comes out falsy gets left out of the new list. Same comprehension you already know, with a doorman on it. 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.
for clauses in one comprehension flatten a list of lists into a single flat list: [item for row in matrix for item in row]. Read it left to right, outer loop first, exactly the order you'd write the loops. If your eyes snag on it, that's your cue to write the real loops instead. 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, the same as 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}{key: value for item in iterable} builds a dictionary in one line, same shape as a list comprehension with a colon between key and value. Pair it with .items() to reshape a dict you already have, or with zip() to stitch two lists into one mapping. Add an if at the end to keep only the pairs you want. 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.
{expr for item in iterable} with curly braces and no colon builds a set, and a set throws out duplicates for free. So if your job is "give me the unique ones", this does it in a line. Don't count on any particular order coming back though. 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.
sum() or max() and skip building the list at all. 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)*pairs unpacks the list into separate arguments, so 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})")zip() pairs up two or more sequences so you can loop them together, no index juggling. It stops at the shortest one, so mismatched lengths quietly lose the extras. And zip(*pairs) runs it in reverse, splitting a list of tuples back into separate lists. 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)map(func, iterable) runs a function over every item; filter(func, iterable) keeps only the items where the function comes back truthy. They're the older way to do what comprehensions do, so you'll meet them in other people's code. For your own, a comprehension reads clearer to most folks. 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}")
