Comprehensions and Walrus

Updated

September 8, 2026

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.py

Output:

[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.py

Output:

{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.py

Output:

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.py

Output:

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.py

Output:

[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.py

Output:

[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 optional if, 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 in if.
  • No side effects inside a comprehension.

Try this

  1. In open_ids.py, also build a set of open statuses with a set comprehension. It should print {'open'}.
  2. Add a "void" ticket to sum_open.py and confirm any_void becomes True.
  3. Rewrite cover_cap.py’s if without := using n = len(covers) on the previous line. Keep the output.