Comprehensions and Walrus
Comprehensions and Walrus
A comprehension builds a list, dict, or set from an iterable in one expression. A generator expression does the same walk without building the collection up front. The walrus operator := assigns inside an expression. The boring default is a one-line filter or map you can read aloud. Nested comprehensions and walrus-in-every-if are how a desk script becomes a crossword.
Mental model
[expression for item in items if condition]
That is a loop and an optional filter, producing a list. Change [] to {} with key: value for a dict, or {expression} for a set. Parentheses (expression for item in items) make a generator expression: lazy, one item at a time, useful in sum, any, all.
:= names a value in the middle of an if or while so you do not compute it twice. if (n := len(tickets)) > 10: binds n and tests it. The parentheses are required in an if.
Comprehensions are not a second language. If the expression needs two fors, a function call with side effects, or a comment, write a loop.
Worked examples
Case 1: List comprehension as a filter
Save as open_ids.py.
# open_ids.py
def main():
tickets = [
{"id": 7, "status": "open", "cents": 1850},
{"id": 8, "status": "paid", "cents": 400},
{"id": 9, "status": "open", "cents": 0},
]
open_ids = [t["id"] for t in tickets if t["status"] == "open"]
print(open_ids)
if __name__ == "__main__":
main()Run:
uv run python open_ids.pyOutput:
[7, 9]
Read it: “the id for each ticket if the status is open.” That sentence is the test. If you cannot say it, do not write it as a comprehension.
Case 2: Dict and set comprehensions
# index_tables.py
def main():
tickets = [
{"id": 7, "table": 12},
{"id": 8, "table": 3},
{"id": 9, "table": 12},
]
by_id = {t["id"]: t["table"] for t in tickets}
tables = {t["table"] for t in tickets}
print(by_id)
print(sorted(tables))
if __name__ == "__main__":
main()Run:
uv run python index_tables.pyOutput:
{7: 12, 8: 3, 9: 12}
[3, 12]
Dict keys must be unique: a second 7 would overwrite. The set drops the duplicate table 12. Set print order is not a promise, so this program sorts before printing.
Case 3: Generator expression for a total
# sum_open.py
def main():
tickets = [
{"status": "open", "cents": 1850},
{"status": "paid", "cents": 400},
{"status": "open", "cents": 250},
]
total = sum(t["cents"] for t in tickets if t["status"] == "open")
print(total)
any_void = any(t["status"] == "void" for t in tickets)
print(any_void)
if __name__ == "__main__":
main()Run:
uv run python sum_open.pyOutput:
2100
False
sum(...) takes a generator expression without extra parentheses when it is the only argument. You do not keep a list of cents you will never use again.
Case 4: Walrus to name a length once
# cover_cap.py
def main():
covers = [2, 4, 2, 5, 3]
if (n := len(covers)) > 4:
print(f"{n} parties waiting")
else:
print(f"{n} parties, under cap")
while (n := len(covers)) > 3:
covers.pop()
print("sat one,", n - 1, "left")
if __name__ == "__main__":
main()Run:
uv run python cover_cap.pyOutput:
5 parties waiting
sat one, 4 left
sat one, 3 left
The if uses n in the message. The while re-reads the length each pass. That is the honest use: compute, name, test, without a duplicate len(covers).
The trap
Nested comprehensions that zip the whole desk into one line. They run. Nobody wants to edit them on a Saturday.
# too_clever_comp.py
def main():
shifts = [
{"day": "mon", "tickets": [{"id": 1, "ok": True}, {"id": 2, "ok": False}]},
{"day": "tue", "tickets": [{"id": 3, "ok": True}]},
]
ids = [
t["id"]
for shift in shifts
if shift["day"] != "sun"
for t in shift["tickets"]
if t["ok"]
]
print(ids)
if __name__ == "__main__":
main()Run:
uv run python too_clever_comp.pyOutput:
[1, 3]
The two fors are a nested loop with the outer for first. That is the opposite of how people read English nested phrases. The boring fix is a loop, or a small function:
# ok_ids.py
def ok_ids(shifts):
ids = []
for shift in shifts:
if shift["day"] == "sun":
continue
for t in shift["tickets"]:
if t["ok"]:
ids.append(t["id"])
return ids
def main():
shifts = [
{"day": "mon", "tickets": [{"id": 1, "ok": True}, {"id": 2, "ok": False}]},
{"day": "tue", "tickets": [{"id": 3, "ok": True}]},
]
print(ok_ids(shifts))
if __name__ == "__main__":
main()Run:
uv run python ok_ids.pyOutput:
[1, 3]
Same ids. You can put a breakpoint on continue. Nested comps also hide side effects (print, appends to an outer list). Do not put those in the expression.
Walrus has a twin trap: if n := len(covers) > 4 without parentheses binds n to True/False, not the length. Always parenthesize := in if.
The boring rule
- One
for, one optionalif, a cheap expression: comprehension. - Totals and any/all: generator expression.
- Two or more
fors: nested loops or a helper function. :=when it removes a duplicate call, with parentheses inif.- No side effects inside a comprehension.
Try this
- In
open_ids.py, also build a set of open statuses with a set comprehension. It should print{'open'}. - Add a
"void"ticket tosum_open.pyand confirmany_voidbecomesTrue. - Rewrite
cover_cap.py’sifwithout:=usingn = len(covers)on the previous line. Keep the output.