List Comprehensions — Pythonic Collection Building

A list comprehension is Python’s concise syntax for creating a new list by applying an expression to every item in an iterable, with an optional filter condition. Instead of writing a for loop that appends to an empty list, a comprehension does it in a single, readable line. They are one of the most distinctly Pythonic constructs — you will see them everywhere in Python code, including FastAPI response transformations, SQLAlchemy result processing, and data pipeline logic. The key is knowing when a comprehension improves readability and when a regular loop is clearer — comprehensions should be simple enough to read in a single glance.

List Comprehension Syntax

# Syntax: [expression for item in iterable if condition]
# The 'if condition' part is optional

# ── Basic: transform every item ────────────────────────────────────────────────
numbers = [1, 2, 3, 4, 5]

# Traditional for loop
squares = []
for n in numbers:
    squares.append(n ** 2)
# squares = [1, 4, 9, 16, 25]

# List comprehension — one line
squares = [n ** 2 for n in numbers]
# [1, 4, 9, 16, 25]

# ── With filter: only include some items ───────────────────────────────────────
# Traditional
evens = []
for n in numbers:
    if n % 2 == 0:
        evens.append(n)

# Comprehension
evens = [n for n in numbers if n % 2 == 0]
# [2, 4]

# ── Transform AND filter ───────────────────────────────────────────────────────
even_squares = [n ** 2 for n in numbers if n % 2 == 0]
# [4, 16]
Note: A list comprehension always creates a new list — it never modifies the original iterable. The expression runs once for every item that passes the condition filter. The variable name inside the comprehension (n, item, post) is local to the comprehension in Python 3 — it does not leak into the surrounding scope, unlike Python 2 where loop variables from comprehensions would overwrite outer variables of the same name.
Tip: The readability rule for comprehensions: if it fits comfortably on one line and reads naturally from left to right, use a comprehension. If you need nested logic, multiple conditions, or complex expressions, use a regular for loop with a descriptive variable name. A 120-character comprehension with two nested conditions is harder to read than 5 lines of clear loop code. Comprehensions are a tool for clarity, not a goal in themselves.
Warning: List comprehensions build the entire list in memory before you can use it. If you are processing millions of items, use a generator expression instead (covered in the next lesson) — it produces items one at a time without building the whole list. In FastAPI, never use a list comprehension to transform a million-row database result — use pagination and generators instead.

Practical FastAPI Examples

# ── Extracting fields from database results ────────────────────────────────────
db_posts = [
    {"id": 1, "title": "First",  "body": "...", "author_id": 1, "published": True},
    {"id": 2, "title": "Second", "body": "...", "author_id": 2, "published": False},
    {"id": 3, "title": "Third",  "body": "...", "author_id": 1, "published": True},
]

# Get titles of published posts
published_titles = [p["title"] for p in db_posts if p["published"]]
# ["First", "Third"]

# Build a list of IDs
post_ids = [p["id"] for p in db_posts]
# [1, 2, 3]

# ── Transforming response data ─────────────────────────────────────────────────
# Add a "slug" field based on the title
def to_slug(title: str) -> str:
    return title.lower().replace(" ", "-")

posts_with_slugs = [
    {**p, "slug": to_slug(p["title"])}   # unpack dict and add slug field
    for p in db_posts
]
# [{"id":1, "title":"First", ..., "slug":"first"}, ...]

# ── String processing ──────────────────────────────────────────────────────────
tags_input = "  python , fastapi , postgresql  "
tags = [tag.strip() for tag in tags_input.split(",")]
# ["python", "fastapi", "postgresql"]

# Normalise tags: lowercase, strip, remove empty
raw_tags = ["Python", " FastAPI ", "", "PostgreSQL", "python"]
clean_tags = list({tag.strip().lower() for tag in raw_tags if tag.strip()})
# ["python", "fastapi", "postgresql"] (unique, sorted)

Comprehension with Conditionals — if/else Inside

# The filter 'if' goes at the END of the comprehension
# An if/else INSIDE the expression goes BEFORE the for

numbers = [1, 2, 3, 4, 5]

# Filter (if at end — keeps only some items)
odds = [n for n in numbers if n % 2 != 0]
# [1, 3, 5]

# Transform with ternary (if/else before for — transforms ALL items)
labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
# ["odd", "even", "odd", "even", "odd"]

# Combine: transform then filter
result = [n * 10 if n > 2 else n for n in numbers if n != 4]
# n=1: 1 (not >2), n=2: 2 (not >2), n=3: 30 (>2), n=5: 50 (>2)
# [1, 2, 30, 50]  (4 was filtered out)

Nested List Comprehensions

# Nested comprehension — for 2D structures
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Flatten a 2D list to 1D
flat = [num for row in matrix for num in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Read: "for each row in matrix, for each num in row, take num"

# Transpose a matrix
transposed = [[row[i] for row in matrix] for i in range(3)]
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]

# Simple nested — all combinations
colours = ["red", "blue"]
sizes   = ["S", "M", "L"]
combos  = [f"{c}-{s}" for c in colours for s in sizes]
# ["red-S", "red-M", "red-L", "blue-S", "blue-M", "blue-L"]

Common Mistakes

Mistake 1 — Confusing filter position with ternary position

❌ Wrong — putting ternary after the for (syntax error) or filter before for:

[n for n in numbers if n % 2 == 0 else n * 10]  # SyntaxError

✅ Correct — ternary goes BEFORE the for, filter goes AFTER:

[n if n % 2 == 0 else n * 10 for n in numbers]  # transform all ✓
[n for n in numbers if n % 2 == 0]              # filter only ✓

Mistake 2 — Over-complex comprehensions

❌ Wrong — unreadable one-liner:

result = [f(x) for x in data if g(x) and h(x) for y in x.items() if y[0] != "skip"]

✅ Correct — use a regular for loop when logic is complex:

result = []
for x in data:
    if g(x) and h(x):
        for key, val in x.items():
            if key != "skip":
                result.append(f(x))
# Clear intent, easy to debug ✓

Mistake 3 — Using a comprehension for side effects

❌ Wrong — comprehension just to call a function (wastes memory building a list):

[print(item) for item in items]   # builds a [None, None, ...] list for nothing

✅ Correct — use a for loop for side effects:

for item in items:
    print(item)   # ✓ no wasted list creation

Quick Reference

Pattern Code
Transform all items [expr for x in iterable]
Filter items [x for x in iterable if condition]
Transform + filter [expr for x in iterable if condition]
Ternary in expression [a if cond else b for x in iterable]
Flatten 2D list [item for row in matrix for item in row]
From string list [s.strip().lower() for s in strings]

🧠 Test Yourself

You have a list of post dictionaries. Write a list comprehension that returns only the titles of posts where published is True and the title has more than 5 characters.