List Comprehensions — Python's Superpower

8 min read

A list comprehension turns a four-line for/if/append loop into a single line. Once you've used them you'll wonder how any language gets by without them. They're the most distinctly Pythonic feature in the language — every QA codebase you'll meet uses them to extract test fixtures, filter results, transform fields, and reshape data. This lesson covers list, dict, and set comprehensions, when to reach for them, and the exact moment you should not.

The everyday loop, rewritten

A common shape in test code: take a list of users, build a list of just their names.

The procedural way:

users = [
    {"name": "Alice", "role": "admin"},
    {"name": "Bob", "role": "tester"},
    {"name": "Carol", "role": "admin"},
]
 
names = []
for user in users:
    names.append(user["name"])
 
print(names)        # ['Alice', 'Bob', 'Carol']

Four lines of scaffolding around what is really one idea: "for each user, take the name." A list comprehension says exactly that, in one line:

names = [user["name"] for user in users]
print(names)        # ['Alice', 'Bob', 'Carol']

The brackets [ ] produce a new list. Inside, you write the expression first, then the for clause that drives it. Read it as: "the namesuser["name"]for each user in users."

The general syntax

[expression for item in iterable]
[expression for item in iterable if condition]

A few examples:

status_codes = [200, 201, 404, 500, 200, 503]
 
# Transformation: square each number
doubled = [n * 2 for n in status_codes]
# [400, 402, 808, 1000, 400, 1006]
 
# Filtering: only successful codes
ok = [n for n in status_codes if n < 400]
# [200, 201, 200]
 
# Transformation + filtering combined
ok_labels = [f"OK-{n}" for n in status_codes if n < 400]
# ['OK-200', 'OK-201', 'OK-200']

The position of the if matters — it goes after the for clause and acts as a filter. Only items that pass the condition contribute to the new list.

A real QA example — extract admin emails

A pattern you'll write twenty times in your first month: pull a specific field from records that match a condition.

users = [
    {"name": "Alice", "email": "alice@test.com", "role": "admin"},
    {"name": "Bob",   "email": "bob@test.com",   "role": "tester"},
    {"name": "Carol", "email": "carol@test.com", "role": "admin"},
    {"name": "Dan",   "email": "dan@test.com",   "role": "tester"},
]
 
admin_emails = [u["email"] for u in users if u["role"] == "admin"]
print(admin_emails)        # ['alice@test.com', 'carol@test.com']

That single line replaces this:

admin_emails = []
for u in users:
    if u["role"] == "admin":
        admin_emails.append(u["email"])

Both work. The comprehension is shorter, faster (slightly — Python's interpreter optimises the loop), and reads more like the question being asked.

Comprehensions vs map and filter

If you're coming from JavaScript, comprehensions are roughly map + filter combined:

GoalJavaScriptPython
Transform eachusers.map(u => u.email)[u["email"] for u in users]
Filterusers.filter(u => u.role === "admin")[u for u in users if u["role"] == "admin"]
Filter + transformusers.filter(u => u.role === "admin").map(u => u.email)[u["email"] for u in users if u["role"] == "admin"]

Python does have map() and filter() built in, but most code uses comprehensions instead — they read more cleanly and don't need a lambda for the body.

Dictionary comprehensions

Same idea, different brackets — {} and a key: value shape:

codes = [200, 201, 404, 500]
status_map = {code: ("success" if code < 400 else "error") for code in codes}
# {200: 'success', 201: 'success', 404: 'error', 500: 'error'}

A QA-flavoured one — turn a list of test results into a name → status lookup:

results = [
    {"name": "login",    "status": "PASS"},
    {"name": "checkout", "status": "FAIL"},
    {"name": "search",   "status": "PASS"},
]
 
by_name = {r["name"]: r["status"] for r in results}
# {'login': 'PASS', 'checkout': 'FAIL', 'search': 'PASS'}

Set comprehensions

{} again, but with no : — Python infers from the shape:

users = [
    {"name": "Alice", "role": "admin"},
    {"name": "Bob",   "role": "tester"},
    {"name": "Carol", "role": "admin"},
]
 
unique_roles = {u["role"] for u in users}      # {'admin', 'tester'}

Sets are perfect for distinct values — deduplication in one expression, no manual tracking required.

The two-line vs one-line rule

Comprehensions are powerful enough to abuse. A small QA-specific rule that holds up:

  • One iterable, simple expression, optional simple filter → comprehension.
  • Multiple conditions, nested loops, side effects, or anything spanning two lines → regular for loop.

This compiles but few would defend it:

# Hard to read at a glance
labels = [f"{u['name'].upper()}({r['status']})" for u in users for r in results if u['name'] == r.get('owner') and r['status'] == 'FAIL' and u['role'] != 'guest']

A regular for loop with a couple of ifs reads better. Conciseness is not a goal in itself.

A note on nested comprehensions

You can stack for clauses to combine sequences (the equivalent of a Cartesian product):

browsers = ["Chrome", "Firefox", "Safari"]
resolutions = ["1920x1080", "1366x768"]
 
combos = [(b, r) for b in browsers for r in resolutions]
# [('Chrome', '1920x1080'), ('Chrome', '1366x768'),
#  ('Firefox', '1920x1080'), ('Firefox', '1366x768'),
#  ('Safari', '1920x1080'), ('Safari', '1366x768')]

That's six (browser, resolution) pairs — exactly the kind of test matrix you'd run a Playwright suite over. Two for clauses in one comprehension stay readable; three is usually a sign you should switch to a regular loop.

Side-by-side: loop vs comprehension

Same job — extract failed test names — two ways

Procedural for loop

  • Five lines: setup, for, if, append, end

  • Imperative — describes HOW, step by step

  • Easy to add a print() in the middle for debugging

  • Good when the body has multiple statements or branches

  • Reads as: 'create empty list, loop, check, add, repeat'

List comprehension

  • One line: [name for r in results if r.status == 'FAIL']

  • Declarative — describes WHAT you want

  • Slightly faster (Python optimises the loop)

  • Best when the body is a single expression with one optional filter

  • Reads like a sentence: 'names of failed results'

Both forms are valid Python. The right one is whichever a reviewer can read at a glance — a single-expression comprehension wins; anything more, the loop does.

A medium QA example — process 50 results

A realistic chunk of post-run analysis using comprehensions throughout:

results = [
    {"name": "login",    "status": "PASS", "duration_ms": 1240, "priority": "P0"},
    {"name": "checkout", "status": "FAIL", "duration_ms": 980,  "priority": "P0"},
    {"name": "search",   "status": "PASS", "duration_ms": 320,  "priority": "P2"},
    {"name": "logout",   "status": "PASS", "duration_ms": 410,  "priority": "P1"},
    {"name": "profile",  "status": "FAIL", "duration_ms": 5200, "priority": "P1"},
]
 
failed_names = [r["name"] for r in results if r["status"] == "FAIL"]
slow_tests = [r["name"] for r in results if r["duration_ms"] > 1000]
p0_failures = [r["name"] for r in results if r["status"] == "FAIL" and r["priority"] == "P0"]
durations_by_name = {r["name"]: r["duration_ms"] for r in results}
distinct_priorities = {r["priority"] for r in results}
 
print(f"Failed:        {failed_names}")
print(f"Slow (>1s):    {slow_tests}")
print(f"P0 failures:   {p0_failures}")
print(f"Distinct pri.: {distinct_priorities}")

Output:

Failed:        ['checkout', 'profile']
Slow (>1s):    ['login', 'profile']
P0 failures:   ['checkout']
Distinct pri.: {'P0', 'P1', 'P2'}

Five different shapes of question, five one-liners. That brevity is why list comprehensions are the single feature most Python developers miss when they switch back to other languages.

⚠️ Common mistakes

  • Side effects inside a comprehension. [print(r["name"]) for r in results] works but is bad style — you're building a list of Nones just for the side effect. Use a regular for loop when you only want to print or mutate; comprehensions are for building values.
  • Forgetting that filter goes after for. [u if u["role"] == "admin" for u in users] is a SyntaxError. The filter form is [u for u in users if u["role"] == "admin"]. The standalone if after the expression (without else) is reserved for ternaries — different syntax.
  • Nesting too deeply. Two for clauses are fine; three start to hurt; four is usually a signal to refactor into helper functions. If a reviewer has to trace which for binds which variable, switch to a regular loop.

🎯 Practice task

Replace loops with comprehensions over real test data. 20-30 minutes.

  1. Create result_analysis.py. Define results as a list of at least 8 test-result dicts with keys name, status ("PASS" or "FAIL"), duration_ms, and priority ("P0" / "P1" / "P2").
  2. Using list comprehensions, build:
    • failed_names — list of names of failed tests
    • slow_tests — list of names where duration_ms > 1000
    • p0_failures — list of names that are FAIL and P0
    • durations — list of just the duration_ms values
  3. Using a dict comprehension, build by_name{name: status} for every result.
  4. Using a set comprehension, build priorities — set of distinct priority strings.
  5. Print every collection. Run with python result_analysis.py.
  6. Pick one of the comprehensions and rewrite it as an explicit for loop with append. Compare the two side by side. Notice the trade-off: the comprehension is shorter; the loop is easier to step through with print debugging.
  7. Stretch: combine two iterables in a comprehension to produce a test matrix. Define browsers = ["chromium", "firefox", "webkit"] and viewports = [(1920, 1080), (1366, 768)]. Build combos = [(b, w, h) for b in browsers for (w, h) in viewports]. Confirm the length is 6 and print each combo on its own line.

You can now express filter, transform, and reshape jobs in one readable line — the most Pythonic skill in this whole course. The next chapter zooms in on Python's built-in collections in depth: lists, dicts, tuples, sets, and JSON.

// tip to track lessons you complete and pick up where you left off across devices.