| name | python-bio-comprehensions |
| description | Write Python comprehensions/generator expressions to filter, transform, count DNA/RNA/protein sequences (GC%, codons, k-mers, ORFs). Use when refactoring loop-heavy sequence code or streaming FASTA/FASTQ memory-efficiently. |
| tool_type | python |
| primary_tool | Python |
Python Comprehensions for Bioinformatics
When to Use
- Refactoring an explicit
for loop that builds a list/dict/set of sequences, positions, or stats into a single expression.
- Filtering a FASTA dict (name → sequence) by length, GC content, or motif presence.
- Computing per-sequence stats (GC%, k-mer spectrum, codon counts) across a collection without intermediate loops.
- Streaming a genome-scale file (FASTQ, VCF, large FASTA) where materializing a full list would blow up memory — use a generator expression instead.
- Generating combinatorial sequences (all k-mers, all codons, reading frames) for enumeration or lookup tables.
Version Compatibility
Pure standard library — Python ≥3.8 (walrus operator := in comprehensions requires ≥3.8; everything else works on ≥3.6). No third-party dependencies.
Prerequisites
- Comfortable with plain
for loops, dict/set literals, and basic string slicing in Python.
- No packages to install — only
itertools and collections from the standard library are used below.
Core Patterns
Goal: turn a for-loop that builds a list/dict/set into a single comprehension, and know when a filter (if after for) is not the same as a transform (if/else in the expression).
Approach: [expr for item in iterable if condition] filters (drops items); [expr_if_true if condition else expr_if_false for item in iterable] transforms (keeps every item, changes its value). Nest by writing the outer for first; use [[...] for outer in ...] when you need a list-of-lists instead of a flattened one.
def gc_content(seq: str) -> float:
"""Fraction of a sequence that is G or C (0.0-1.0)."""
return (seq.upper().count('G') + seq.upper().count('C')) / len(seq)
def classify_bases(dna: str) -> list[str]:
"""Filter vs transform, side by side."""
gc_only = [nt for nt in dna if nt in 'GC']
purine_map = ['purine' if nt in 'AG' else 'pyrimidine' for nt in dna]
return gc_only, purine_map
def filter_fasta(fasta: dict[str, str], min_len: int = 100,
gc_range: tuple[float, float] = (0.4, 0.6)) -> dict[str, str]:
"""Keep only sequences that are long enough and within a GC window."""
{name: seq name, seq fasta.items()
(seq) > min_len gc_range[] <= gc_content(seq) <= gc_range[]}
Goal: process genome-scale data (FASTQ reads, chromosome-length sequences) without materializing an intermediate list in memory.
Approach: swap [...] for (...) — a generator expression is lazily evaluated and single-pass. Feed it straight into sum, max, any, all, or a for loop; never call list() on it unless you actually need to index or iterate it twice.
def high_gc_fraction(sequences, threshold: float = 0.55) -> float:
"""Fraction of sequences above a GC threshold, computed with zero
intermediate list — only one sequence is in memory at a time.
"""
total = len(sequences)
n_high_gc = sum(1 for seq in sequences if gc_content(seq) > threshold)
return n_high_gc / total
def average_gc(sequences) -> float:
"""sum()/max()/any()/all() all accept a generator directly -- no [] needed."""
return sum(gc_content(s) for s in sequences) / len(sequences)
Goal: generate k-mers, codons, and reading frames for enumeration, spectra, or lookup tables.
Approach: use itertools.product for combinatorial generation, nested for clauses for reading frames (list-of-lists), and collections.Counter — not a comprehension calling .count() — for k-mer tallies.
from itertools import product
from collections import Counter
GENETIC_CODE = {
'ATG': 'M', 'TAA': '*', 'TAG': '*', 'TGA': '*', 'TTT': 'F',
}
def all_kmers(k: int, alphabet: str = 'ATGC') -> list[str]:
"""All possible k-mers over an alphabet (4**k of them for DNA)."""
return [''.join(c) for c in product(alphabet, repeat=k)]
def kmer_spectrum(seq: str, k: int) -> Counter:
"""Counter, not {kmer: kmers.count(kmer) for kmer in set(kmers)} -- that
dict comprehension is O(n^2): it rescans the list once per unique key.
"""
kmers = [seq[i:i + k] for i in range(len(seq) - k + 1)]
return Counter(kmers)
def reading_frames(dna: str) -> list[list[str]]:
"""Three forward reading frames as a list of codon lists (nested comprehension)."""
[[dna[i:i + ] i (frame, (dna) - , )] frame ()]
() -> [[, , ]]:
start_pos = [i i ((dna) - ) dna[i:i + ] starts]
stop_pos = [i i ((dna) - ) dna[i:i + ] stops]
orfs = []
s start_pos:
e stop_pos:
e > s (e - s) % == :
orfs.append((s, e + , dna[s:e + ]))
orfs
__name__ == :
(gc_content(), ) ==
filter_fasta({: * , : * }) == {: * }
kmer_spectrum(, ) == Counter({: , : })
reading_frames()[] == [, , ]
find_orfs() == [(, , )]
()
Pitfalls
- Nested comprehension loop order: in a flat nested comprehension the outer
for comes first — [expr for outer in outer_list for inner in inner_list] flattens; use [[expr for inner in ...] for outer in ...] to keep a list-of-lists.
if filter vs if/else transform: if after for is a filter (removes items); if/else in the expression is a conditional transform (keeps every item, maps it differently). Mixing them up silently drops data.
- Generator expressions are single-pass: once consumed (
sum(...), list(...), one for loop), the generator is exhausted — a second iteration silently yields nothing. Use a list comprehension when you need multiple passes, indexing, or len().
- Dict comprehension with
.count() is O(n²): {kmer: kmers.count(kmer) for kmer in set(kmers)} rescans the full list once per unique key. Use collections.Counter(kmers) instead.
- Off-by-one errors: Python ranges/slices are half-open
[start, stop), but bioinformatics coordinates (GFF, 1-based FASTA positions) are often 1-based inclusive — convert explicitly at the I/O boundary, not inside the comprehension.
- Deep vs shallow copy:
list.copy() and [:] only copy the top level; nested structures (list of lists, dict of dicts) still share inner references. Use copy.deepcopy() when you need full independence.
See Also
bio-sequence-manipulation-sequence-properties — GC content, molecular weight, and other per-sequence metrics via Biopython.
bio-sequence-io-read-sequences — reading FASTA/FASTQ into the dicts/iterables these comprehensions operate on.
bio-sequence-manipulation-codon-usage — codon/amino-acid frequency tables built on the same comprehension patterns.
biopython — reach for Bio.Seq/Bio.SeqUtils once a one-off comprehension becomes a repeated, validated operation.