# Python Bio Comprehensions

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

- Skill: `pavel-kravchenko/python-bio-comprehensions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/python-bio-comprehensions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/python-bio-comprehensions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/python-bio-comprehensions

---


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

```python
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']                       # filter: drops A/T
    purine_map = ['purine' if nt in 'AG' else 'pyrimidine' for nt in dna]  # transform: keeps all, relabels
    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."""
    return {name: seq for name, seq in fasta.items()
            if len(seq) > min_len and gc_range[0] <= gc_content(seq) <= gc_range[1]}
```

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

```python
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)
    # generator expression: not materialized, single-pass, consumed once by sum()
    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.

```python
from itertools import product
from collections import Counter

GENETIC_CODE = {  # abbreviated; use Bio.Seq.Seq.translate() for the full table in practice
    '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)."""
    return [[dna[i:i + 3] for i in range(frame, len(dna) - 2, 3)] for frame in range(3)]


def find_orfs(dna: str, starts=('ATG',), stops=('TAA', 'TAG', 'TGA')) -> list[tuple[int, int, str]]:
    """Pair each start codon with the nearest downstream in-frame stop codon."""
    start_pos = [i for i in range(len(dna) - 2) if dna[i:i + 3] in starts]
    stop_pos = [i for i in range(len(dna) - 2) if dna[i:i + 3] in stops]
    orfs = []
    for s in start_pos:
        for e in stop_pos:
            if e > s and (e - s) % 3 == 0:
                orfs.append((s, e + 3, dna[s:e + 3]))
                break
    return orfs


if __name__ == '__main__':
    assert round(gc_content('GGCC'), 2) == 1.0
    assert filter_fasta({'a': 'AT' * 60, 'b': 'GC' * 60}) == {'b': 'GC' * 60}
    assert kmer_spectrum('ATGATG', 3) == Counter({'ATG': 2, 'TGA': 1})
    assert reading_frames('ATGCGATCG')[0] == ['ATG', 'CGA', 'TCG']
    assert find_orfs('ATGAAATAG') == [(0, 9, 'ATGAAATAG')]
    print('all checks passed')
```

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

