# Python Bio Control Flow

> Write if/elif/for/while loops over DNA/RNA/protein strings: codon iteration, stop-codon/motif scanning, GC-content classification. Use when looping over sequences, extracting codons, or debugging an off-by-one loop.

- Skill: `pavel-kravchenko/python-bio-control-flow` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/python-bio-control-flow`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/python-bio-control-flow/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-control-flow

---


# Control Flow for Bioinformatics

## When to Use

- Iterating over a DNA/RNA/protein string codon-by-codon or residue-by-residue.
- Scanning a sequence or reading frame for a stop codon, start codon, or arbitrary motif.
- Classifying sequences (DNA vs RNA vs protein, GC-content bucket, purine/pyrimidine).
- Filtering a batch of reads/sequences by length, GC%, or composition using `if`/`elif`/`continue`.
- Debugging an off-by-one codon slice, a `break` that only exits one loop, or a `while` that never terminates.

## Version Compatibility

Pure Python standard library only — Python ≥3.8 (walrus operator and f-strings used below need ≥3.8). No third-party packages required.

## Prerequisites

- Comfortable with Python strings, slicing (`seq[i:i+3]`), `set`, and `dict`.
- Know basic sequence concepts: codon = 3 nt, reading frame, GC content, purine (A/G) vs pyrimidine (C/T/U).

**Goal:** iterate a DNA sequence in complete, non-overlapping codons.
**Approach:** use `range(0, len(dna) - 2, 3)` (not `range(len(dna))`) so the loop never starts a codon it can't finish, and translate using a codon table with an explicit stop-codon break.

```python
CODON_TABLE = {
    'TTT': 'F', 'TTC': 'F', 'TTA': 'L', 'TTG': 'L',
    'CTT': 'L', 'CTC': 'L', 'CTA': 'L', 'CTG': 'L',
    'ATT': 'I', 'ATC': 'I', 'ATA': 'I', 'ATG': 'M',
    'GTT': 'V', 'GTC': 'V', 'GTA': 'V', 'GTG': 'V',
    'TCT': 'S', 'TCC': 'S', 'TCA': 'S', 'TCG': 'S',
    'CCT': 'P', 'CCC': 'P', 'CCA': 'P', 'CCG': 'P',
    'ACT': 'T', 'ACC': 'T', 'ACA': 'T', 'ACG': 'T',
    'GCT': 'A', 'GCC': 'A', 'GCA': 'A', 'GCG': 'A',
    'TAT': 'Y', 'TAC': 'Y', 'TAA': '*', 'TAG': '*',
    'CAT': 'H', 'CAC': 'H', 'CAA': 'Q', 'CAG': 'Q',
    'AAT': 'N', 'AAC': 'N', 'AAA': 'K', 'AAG': 'K',
    'GAT': 'D', 'GAC': 'D', 'GAA': 'E', 'GAG': 'E',
    'TGT': 'C', 'TGC': 'C', 'TGA': '*', 'TGG': 'W',
    'CGT': 'R', 'CGC': 'R', 'CGA': 'R', 'CGG': 'R',
    'AGT': 'S', 'AGC': 'S', 'AGA': 'R', 'AGG': 'R',
    'GGT': 'G', 'GGC': 'G', 'GGA': 'G', 'GGG': 'G',
}

def translate(dna: str) -> str:
    """Translate a DNA coding sequence into a protein sequence, stopping at the first stop codon."""
    dna = dna.upper()
    protein = []
    # step 3, and stop 2 short of the end so every slice is a full codon
    for i in range(0, len(dna) - 2, 3):
        codon = dna[i:i + 3]
        amino_acid = CODON_TABLE.get(codon, '?')
        if amino_acid == '*':
            break
        protein.append(amino_acid)
    return ''.join(protein)

assert translate("ATGGCCGATCGTTAG") == "MADR"
```

**Goal:** find the first in-frame stop codon, and separately find every occurrence of an arbitrary motif (which may overlap and isn't frame-locked).
**Approach:** a frame-locked scan advances the index by 3 each step with `while`; a motif scan advances by 1 using `str.find`'s start argument so overlapping hits aren't missed.

```python
def first_stop_codon(dna: str, frame: int = 0) -> int | None:
    """Return the 0-based position of the first in-frame stop codon, or None if none found."""
    stop_codons = {"TAA", "TAG", "TGA"}
    pos = frame
    while pos <= len(dna) - 3:
        if dna[pos:pos + 3] in stop_codons:
            return pos
        pos += 3  # step by whole codon, not by 1
    return None

def find_motif_positions(seq: str, motif: str) -> list[int]:
    """Return every 0-based start position of motif in seq, including overlaps."""
    positions = []
    pos = seq.find(motif)
    while pos != -1:
        positions.append(pos)
        pos = seq.find(motif, pos + 1)  # +1, not +len(motif), to catch overlaps
    return positions

assert first_stop_codon("ATGGCCGATCGATAGCCATAGTTAACG") == 15
assert find_motif_positions("ATGCGATGATCGATGCATG", "ATG") == [0, 6, 12, 16]
```

**Goal:** classify a sequence's identity and GC-content bucket, and validate that every character is a legal base — three related `if`/`elif` and `for-else` patterns bioinformatics code uses constantly.
**Approach:** subset tests (`unique <= set("ATGC")`) for identity; a sorted threshold table for GC bucketing; `for...else` to report "all valid" only when no `break` fired.

```python
def detect_sequence_type(sequence: str) -> str:
    """Classify a sequence as DNA, RNA, protein, or Unknown from its alphabet."""
    unique = set(sequence.upper())
    if unique <= set("ATGC"):
        return "DNA"
    elif unique <= set("AUGC"):
        return "RNA"
    elif unique <= set("ACDEFGHIKLMNPQRSTVWY"):
        return "Protein"
    return "Unknown"

GC_CLASSES = [(30, "AT-rich"), (50, "Moderate"), (60, "High GC")]

def classify_gc(sequence: str) -> tuple[float, str]:
    """Return (gc_percent, label); label is the first bucket whose threshold isn't exceeded."""
    s = sequence.upper()
    gc = (s.count('G') + s.count('C')) / len(s) * 100
    for threshold, label in GC_CLASSES:
        if gc < threshold:
            return gc, label
    return gc, "Very high GC"

def validate_sequence(sequence: str, valid_bases: set[str] = frozenset("ATGC")) -> bool:
    """Print the first invalid position found, or confirm validity via the for-else 'no break' branch."""
    for i, nuc in enumerate(sequence):
        if nuc not in valid_bases:
            print(f"Invalid '{nuc}' at position {i + 1}")  # +1 for 1-based reporting
            return False
    else:
        print("Sequence is valid")
        return True

assert detect_sequence_type("ATGCGATCGATCG") == "DNA"
assert classify_gc("GCGCGCGCGC")[1] == "Very high GC"
```

## Pitfalls

- **Off-by-one in codon loops**: `range(0, len(seq) - 2, 3)` stops so the last slice is still a full 3-char codon; `range(0, len(seq), 3)` produces an incomplete final codon when `len(seq) % 3 != 0`.
- **`elif` vs separate `if`**: use `elif` for mutually exclusive classification (one GC bucket, one sequence type); use separate `if` statements for independent filters (length check AND GC check).
- **`break` exits only the innermost loop**: in nested frame-scanning loops (e.g. looping over 3 reading frames, each scanning codons), a `break` in the inner loop does not stop the outer one — use a flag, `return`, or restructure into a function.
- **`while` without progress**: every `while` loop must modify its condition variable or hit a `break`; forgetting `pos += 3` (or using `+= 1` in a frame-locked scan) either loops forever or misses the frame.
- **Motif scan off-by-N**: `seq.find(motif, pos + 1)` finds overlapping motifs; `seq.find(motif, pos + len(motif))` silently skips overlaps — pick deliberately.
- **`pass` does nothing**: it's a syntactic placeholder only — don't use it where you mean `continue` (skip this iteration) or `break`.
- **Mutable default arguments**: `def f(bases=[])` shares one list across every call; use `def f(bases=None)` and initialize inside, or a `frozenset` default as shown above.
- **1-based vs 0-based reporting**: Python indices are 0-based; bioinformatics coordinates (VCF, GenBank, user-facing reports) are conventionally 1-based — add 1 only when *printing*, keep 0-based internally for slicing.

## See Also

- `bio-sequence-manipulation-codon-usage` — codon frequency/usage bias tables built on the same iteration pattern.
- `bio-sequence-manipulation-transcription-translation` — full transcription/translation pipeline (Biopython `Seq.translate`).
- `bio-sequence-manipulation-motif-search` — regex- and Biopython-based motif finding beyond plain `str.find`.
- `bio-sequence-io-filter-sequences` — filtering FASTQ/FASTA records by length/quality at scale.

