Skip to content
v0.1.0latest release21 skills2 categories3 sub-categories22 tested scripts68 reference docsContact us

Comprehension Patterns

Reference for python-comprehension-refactorer · 479 lines

Programming / Pythonv1.0.0Updated OCT 20 20265 references1 script

Every comprehension form, with the loop it replaces. Examples use LLM, RAG and ML code but every pattern is general Python. Python 3.9+ unless a version tag says otherwise.

Contents

Anatomy and grammar

text
[ ELEMENT  for TARGET in ITERABLE  (for TARGET in ITERABLE | if CONDITION)* ]
{ KEY: VALUE  for ... }          dict comprehension
{ ELEMENT  for ... }             set comprehension
( ELEMENT  for ... )             generator expression (lazy)

The clauses after the element are read exactly like nested statements, top to bottom:

python
result = [f(x, y) for x in xs if p(x) for y in ys if q(x, y)]

# is equivalent to
result = []
for x in xs:
    if p(x):
        for y in ys:
            if q(x, y):
                result.append(f(x, y))
PartRules
elementany expression; a conditional expression needs no parentheses, a tuple does: [(i, x) for ...]
targeta name, tuple or starred unpack: for i, (a, b) in enumerate(pairs)
first iterableevaluated once, immediately, in the enclosing scope
later iterablesevaluated once per outer item, inside the comprehension scope
ifany number, AND-ed together; each applies to the clauses before it
walrus :=allowed in element and conditions (3.8+), not in the iterable expression, never rebinding an iteration variable
await / async foronly inside async def (and in asyncio REPL)

There is no tuple comprehension. (x for x in xs) is a generator; use tuple(x for x in xs).

Mapping

Transform every item, keep all of them.

python
# loop
cleaned = []
for doc in docs:
    cleaned.append(doc.page_content.strip())

# comprehension
cleaned = [doc.page_content.strip() for doc in docs]

More mapping examples:

python
messages = [{"role": "user", "content": q} for q in questions]
lengths = [len(tokenizer.encode(t)) for t in texts]
names = [tool["function"]["name"] for tool in tool_schemas]
normalized = [v / norm for v, norm in zip(vectors, norms)]
ids = [f"chunk-{doc_id}-{i}" for i, _ in enumerate(chunks)]

When the mapping function already exists and takes one argument, map is equally fine:

python
stripped = list(map(str.strip, lines))       # fine, often fastest
stripped = [line.strip() for line in lines]  # also fine
stripped = list(map(lambda s: s.strip(), lines))  # avoid: slower and noisier

Filtering

Keep items that pass a test. The if goes after the for.

python
# loop
relevant = []
for hit in hits:
    if hit.score >= 0.75:
        relevant.append(hit)

relevant = [hit for hit in hits if hit.score >= 0.75]

Filter plus map:

python
user_texts = [m["content"] for m in messages if m["role"] == "user"]
non_empty = [line.rstrip("\n") for line in lines if line.strip()]
python_files = [p for p in root.rglob("*.py") if "venv" not in p.parts]

Multiple conditions: chained ifs and and are equivalent; prefer and once you have two.

python
good = [c for c in chunks if c.score > 0.5 if c.lang == "en"]
good = [c for c in chunks if c.score > 0.5 and c.lang == "en"]

continue guards in a loop become negated conditions:

python
for hit in hits:
    if hit.score < min_score:
        continue
    out.append(hit.text)

out = [hit.text for hit in hits if hit.score >= min_score]
# Note: not (a < b) and (a >= b) differ when a or b is NaN. If scores can be NaN,
# write `if not hit.score < min_score` to keep the loop's exact behaviour.

filter with None drops falsy values:

python
non_empty = list(filter(None, texts))  # same as [t for t in texts if t]

Conditional expressions (choose, do not drop)

An if/else before the for chooses a value for every item; it never removes items.

python
labels = ["relevant" if s >= threshold else "irrelevant" for s in scores]

# an if/elif/else chain becomes a chained conditional expression
tiers = ["high" if s > 0.8 else "mid" if s > 0.5 else "low" for s in scores]

Combine both forms: choose, then filter.

python
clipped = [min(s, 1.0) if s > 0 else 0.0 for s in scores if s is not None]

More than one elif belongs in a helper function or a lookup:

python
def tier(score: float) -> str:
    if score > 0.9:
        return "excellent"
    if score > 0.75:
        return "good"
    if score > 0.5:
        return "fair"
    return "poor"

tiers = [tier(s) for s in scores]

Common mistakes:

python
# [x for x in xs if x > 0 else 0]    -> SyntaxError: else not allowed in the filter position
# [x if x > 0 for x in xs]           -> SyntaxError: conditional expression needs else
[x if x > 0 else 0 for x in xs]      # correct: choose
[x for x in xs if x > 0]             # correct: drop

Nested loops and flattening

The for clauses appear in the same order as the nested loops.

python
# flatten a list of lists
flat = [tok for sent in sentences for tok in sent]

# flatten tool calls across responses (None-safe)
calls = [call for resp in responses for call in (resp.tool_calls or [])]

# cartesian product with a filter
pairs = [(q, d) for q in queries for d in docs if d.lang == q.lang]

Loops that extend become an extra for clause:

python
texts = []
for doc in docs:
    texts.extend(splitter.split_text(doc.page_content))

texts = [t for doc in docs for t in splitter.split_text(doc.page_content)]

Flattening alternatives:

WayWhen
[x for xs in nested for x in xs]need a list, maybe a filter or transform
list(chain.from_iterable(nested))pure flatten, fastest for many short lists
sum(nested, [])never: quadratic copying
[*a, *b, *c]a fixed, small number of known lists
recursive generatorarbitrary depth (see generators-and-itertools.md)

A common bug is reversing the clause order:

python
[x for x in row for row in matrix]   # NameError (or uses a stale `row`): row is used before defined
[x for row in matrix for x in row]   # correct

Nested comprehensions (building 2-D structures)

A comprehension inside the element builds a nested structure; a second for clause flattens. Do not confuse them.

python
matrix = [[0.0] * dim for _ in range(n)]             # n independent rows
bad = [[0.0] * dim] * n                               # n references to ONE row
transpose = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
transpose = [list(col) for col in zip(*matrix)]       # usually clearer
attention_mask = [[1 if tok != pad_id else 0 for tok in seq] for seq in batch]

One level of nesting is fine. Two levels (a comprehension in a comprehension in a comprehension) should be unrolled or split into named steps. See when-not-to-use.md.

The walrus operator in comprehensions

:= (3.8+) binds a name inside the comprehension so an expensive value is computed once and reused.

python
# compute once, filter on it, keep it
answers = [parsed for raw in outputs if (parsed := try_parse_json(raw)) is not None]

# avoid double tokenization
fits = [(t, n) for t in texts if (n := len(tokenizer.encode(t))) <= max_tokens]

# equivalent without walrus: a nested generator
fits = [(t, n) for t, n in ((t, len(tokenizer.encode(t))) for t in texts) if n <= max_tokens]

Rules and gotchas:

RuleDetail
binds in the enclosing scopeafter the comprehension parsed holds the last value computed (like a loop variable would)
cannot rebind an iteration variable[x := 1 for x in xs] is a SyntaxError
not in the iterable expression[y for x in (z := data)] is a SyntaxError
not in a class-body comprehensionSyntaxError: assignment expression within a comprehension cannot be used in a class body
parenthesizeif (y := f(x)) reads better; required in some positions

A running state with walrus (works, but itertools.accumulate is clearer):

python
total = 0
running = [total := total + n for n in token_counts]

Dict comprehensions

python
vectors = {rec["id"]: rec["embedding"] for rec in records}
by_name = {tool.name: tool for tool in tools}
config = {k: v for k, v in raw_config.items() if v is not None}
lowercased = {k.lower(): v for k, v in headers.items()}
token_to_id = {tok: i for i, tok in enumerate(vocab)}

Behaviour to know:

  • Duplicate keys: the last one wins, exactly like repeated d[k] = v. If duplicates are a bug, check len(result) == len(source).
  • Key and value are evaluated key first, then value (3.8+).
  • Insertion order is preserved (3.7+).

Dict inversion:

python
id_to_label = {v: k for k, v in label_to_id.items()}   # values must be unique and hashable

# values not unique: invert to lists
from collections import defaultdict
label_to_ids = defaultdict(list)
for doc_id, label in labels.items():
    label_to_ids[label].append(doc_id)

Merging and filtering dicts:

python
merged = {**defaults, **overrides}          # all versions
merged = defaults | overrides               # 3.9+
params = {k: v for k, v in kwargs.items() if k in ALLOWED_PARAMS}

Building a dict from two lists: dict(zip(keys, values)) is clearer than a comprehension. With 3.10+ use zip(keys, values, strict=True) to catch length mismatches.

dict.fromkeys(keys, default) is fine for immutable defaults; never dict.fromkeys(keys, []), all keys share one list. Use {k: [] for k in keys}.

Set comprehensions

python
sources = {c.metadata["source"] for c in chunks}
seen_hashes = {hashlib.sha256(t.encode()).hexdigest() for t in texts}
vocab = {tok for doc in corpus for tok in doc.split()}

A set comprehension dedupes and loses order. To dedupe while keeping first-seen order:

python
unique_in_order = list(dict.fromkeys(items))   # items must be hashable

To dedupe by a key (e.g. near-duplicate chunks by normalized text):

python
seen: set[str] = set()
unique = []
for c in chunks:
    key = c.text.strip().lower()
    if key not in seen:
        seen.add(key)
        unique.append(c)

That loop mutates seen while building unique: keep it a loop. The comprehension trick [c for c in chunks if not (k in seen or seen.add(k))] works but hides a side effect.

Grouping and counting

Counting: use Counter, not a comprehension.

python
from collections import Counter

counts = {}
for tok in tokens:
    counts[tok] = counts.get(tok, 0) + 1

counts = Counter(tokens)
top = counts.most_common(20)
by_role = Counter(m["role"] for m in messages)

Do not count with {t: tokens.count(t) for t in set(tokens)}: that is O(n * unique).

Grouping: one pass with defaultdict(list).

python
from collections import defaultdict

by_doc = {}
for chunk in chunks:
    by_doc.setdefault(chunk.doc_id, []).append(chunk)

by_doc = defaultdict(list)
for chunk in chunks:
    by_doc[chunk.doc_id].append(chunk)
by_doc = dict(by_doc)   # optional: stop auto-creating keys on lookup

Grouping with itertools.groupby needs the data sorted by the same key:

python
from itertools import groupby
from operator import attrgetter

key = attrgetter("doc_id")
by_doc = {doc_id: list(group) for doc_id, group in groupby(sorted(chunks, key=key), key=key)}

The comprehension {k: list(g) for k, g in groupby(...)} is fine because each group is materialised with list before groupby advances. Storing g itself gives empty iterators later.

Aggregating per group in a comprehension after grouping:

python
mean_score = {doc_id: sum(c.score for c in cs) / len(cs) for doc_id, cs in by_doc.items()}

Identity copies and redundant constructors

WriteInstead of
list(xs) / xs.copy() / xs[:][x for x in xs]
set(xs){x for x in xs}
dict(d) / d.copy(){k: v for k, v in d.items()}
list(pairs)[(a, b) for a, b in pairs]
[f(x) for x in xs]list([f(x) for x in xs])
{f(x) for x in xs}set(f(x) for x in xs)
{k: v for k, v in pairs} or dict(pairs)dict([(k, v) for k, v in pairs])

All of these copies are shallow: nested lists and dicts are shared. Use copy.deepcopy for a deep copy.

enumerate, zip and unpacking

python
numbered = [f"{i}. {q}" for i, q in enumerate(questions, start=1)]
pairs = [(q, a) for q, a in zip(questions, answers, strict=True)]   # strict: 3.10+
records = [{"id": i, **meta} for i, meta in enumerate(metadatas)]
heads = [first for first, *_ in rows]
cosine = [sum(a * b for a, b in zip(u, v)) for u, v in pairs]

Async comprehensions

Inside async def you can use async for and await in comprehensions (3.6+).

python
async def collect(stream):
    return [chunk.delta async for chunk in stream if chunk.delta]

async def embed_all(batches, client):
    return [await client.embed(b) async for b in batches]   # sequential, one await at a time

A comprehension of await calls runs them one after another. For concurrency, build the coroutines in a comprehension and gather them:

python
import asyncio

async def embed_concurrently(batches, client, limit=8):
    sem = asyncio.Semaphore(limit)

    async def one(batch):
        async with sem:
            return await client.embed(batch)

    return await asyncio.gather(*(one(b) for b in batches))

Formatting long comprehensions

Up to about 80 characters, one line. Longer: one clause per line, closing bracket on its own line. Black and Ruff format this way.

python
context_blocks = [
    f"[{i}] {hit.metadata['title']}\n{hit.text}"
    for i, hit in enumerate(hits, start=1)
    if hit.score >= min_score
]

If it still does not read well, extract the element into a function:

python
def render_hit(i: int, hit: Hit) -> str:
    return f"[{i}] {hit.metadata['title']}\n{hit.text}"

context_blocks = [render_hit(i, h) for i, h in enumerate(hits, 1) if h.score >= min_score]

Loop to comprehension conversion table

Loop shapeComprehension
out = [] / for x in xs: / out.append(f(x))out = [f(x) for x in xs]
... if p(x): out.append(x)[x for x in xs if p(x)]
... if p(x): continue / out.append(x)[x for x in xs if not p(x)]
... out.append(a if c else b) or if c: append(a) else: append(b)[a if c else b for x in xs]
for x in xs: for y in f(x): out.append(y)[y for x in xs for y in f(x)]
for x in xs: out.extend(f(x))[y for x in xs for y in f(x)]
y = g(x) / if y: out.append(y)[y for x in xs if (y := g(x))]
d = {} / d[k(x)] = v(x){k(x): v(x) for x in xs}
s = set() / s.add(f(x)){f(x) for x in xs}
d[k] = d.get(k, 0) + 1Counter(k for ...)
d.setdefault(k, []).append(v)keep the loop with defaultdict(list)
total += f(x)sum(f(x) for x in xs)
found = False / if p(x): found = True; breakany(p(x) for x in xs)
for x in xs: if p(x): return x / return Nonenext((x for x in xs if p(x)), None)
best = max(...) updating in a loopmax(xs, key=score)

The script find_comprehension_candidates.py detects the first ten shapes automatically and prints the rewrite.

More in Python

python-beginner-concept-tutor

Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.

python-context-manager-writer

Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.

python-dataclass-converter

Convert boilerplate Python classes, dicts, tuples and JSON payloads into dataclasses, or into pydantic v2 BaseModel, TypedDict, NamedTuple, attrs or msgspec when that fits better. Covers every @dataclass option (frozen, slots, kw_only, order, unsafe_hash, match_args, weakref_slot), field() (default_factory, init=False, repr, compare, hash, metadata), __post_init__, InitVar, KW_ONLY, inheritance and default ordering, replace(), asdict deep-copy pitfalls, hashing/equality, serialization and validation. AI focus: LLM request/response and chat Message types, RAG chunks with embeddings (numpy eq gotcha), eval records, configs, typed OpenAI/Anthropic JSON.

python-exception-handling-designer

Design and review Python exception handling: custom exception hierarchies with retryable flags, what to catch where, EAFP vs LBYL, chaining with raise ... from, else/finally, logger.exception, add_note, ExceptionGroup and except* with asyncio.TaskGroup, warnings, asserts vs validation, FastAPI error translation, retries with backoff and jitter, timeouts, cancellation, pytest.raises, plus LLM-app failures: 429 rate limits with retry-after, 5xx overloaded, context-length exceeded, content filters, broken streams, malformed JSON repair loops, tool-call errors fed back to the model, partial embedding batch failures, CUDA OOM batch halving, vector DB outages, circuit breakers and model fallbacks.

python-idiomatic-code-reviewer

Review Python code for non-idiomatic, unpythonic or risky patterns and rewrite it into clean modern Python: PEP 8 / PEP 20, EAFP vs LBYL, iteration idioms (enumerate, zip, comprehensions, any/all), unpacking, f-strings, pathlib, truthiness, `is None`, mutable defaults, walrus, match statements, collections / itertools / functools, logging instead of print, with-blocks and resource handling. Includes AI-engineering checks: hardcoded API keys, time.sleep or requests inside async def, missing HTTP timeouts, unbounded retry loops, model clients built at import time, prompt string concatenation, sequential awaits, eval() on LLM output. Ships a 46-rule AST reviewer with severities and suggested rewrites.

python-type-hints-annotator

Add accurate, modern type hints to Python and fix mypy / pyright (pyrefly, ty) errors: builtin generics, X | None, TypeAlias and the 3.12 type statement, TypeVar and PEP 695 generics, Protocol, Callable / ParamSpec / Concatenate, TypedDict (Required, NotRequired, ReadOnly), Literal, Final, ClassVar, overload, Annotated, TypeGuard / TypeIs, Self, Never, TypeVarTuple, async types, Iterable vs Sequence vs Mapping, TYPE_CHECKING, stubs, py.typed, gradual strictness. AI-engineering focus: typed LLM message dicts and tool schemas, pydantic vs TypedDict, numpy NDArray, torch / jaxtyping shapes, streaming generators, async clients, callbacks, generic VectorStore protocols, typed config. Ships an AST audit: coverage gate, legacy-typing rewrites, inferred types.