# Python Bio Dictionaries

> Use Python dict/defaultdict/Counter/set to translate codons, count k-mers, group genes by chromosome, and compare gene lists (union/intersection). Use when translating DNA, counting k-mers, or comparing gene sets.

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

---


# Dictionaries and Sets in Bioinformatics

## When to Use

- Translating DNA/RNA to protein via a codon lookup table (`GENETIC_CODE` dict).
- Counting nucleotides, k-mers, or codons in a sequence (`Counter`).
- Grouping records by a key without manual `if key not in d` boilerplate (`defaultdict`).
- Comparing gene lists between conditions, studies, or orthologous species (set algebra: `&`, `|`, `-`, `^`).
- Building a small in-memory gene/variant annotation database keyed by ID, or deduplicating a list of sequences/IDs.

## Version Compatibility

Pure Python stdlib (`dict`, `set`, `collections.defaultdict`, `collections.Counter`). No version sensitivity — works unchanged on Python ≥3.8 (dict insertion order, which the examples below rely on, has been guaranteed since 3.7).

## Prerequisites

- No third-party packages required — everything here is `collections` from the standard library.
- Prior concepts: basic Python control flow and string slicing (`seq[i:i+3]`); if working with real FASTA/annotation files, pair with `biopython` or `bio-sequence-io-read-sequences` to get sequences into strings first.

**Goal:** Translate a DNA coding sequence into a protein string using a codon lookup table.
**Approach:** Store the standard genetic code as a `dict[str, str]`, then slice the sequence into codons and look each one up with `.get()` so unrecognized/ambiguous codons don't raise `KeyError`.

```python
GENETIC_CODE = {
    '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):
    """Translate a DNA coding sequence into a protein string.

    Stops at the first in-frame stop codon; unrecognized codons (e.g.
    containing 'N') become 'X'.
    """
    protein = []
    for i in range(0, len(dna) - 2, 3):
        codon = dna[i:i + 3]
        aa = GENETIC_CODE.get(codon, 'X')
        if aa == '*':
            break
        protein.append(aa)
    return ''.join(protein)

cds = "ATGGCCGATCGATAG"
print(translate(cds))  # -> MADR
```

**Goal:** Count nucleotide/k-mer frequencies and compare k-mer profiles between two sequences.
**Approach:** `Counter` is a dict subclass built for counting — it supports `.most_common()` and set-like arithmetic (`&` = min of shared counts, `+` = sum of counts) directly on the counts.

```python
from collections import Counter

def kmer_counts(sequence, k=3):
    """Return a Counter of overlapping k-mers in `sequence`."""
    return Counter(sequence[i:i + k] for i in range(len(sequence) - k + 1))

seq_a = "ATGATGATGCCC"
seq_b = "ATGATGGGGATG"

kmers_a = kmer_counts(seq_a)
kmers_b = kmer_counts(seq_b)

shared = kmers_a & kmers_b     # min of each shared count
combined = kmers_a + kmers_b   # sum of each count
print(kmers_a.most_common(3))
print(dict(shared))

## manual nucleotide frequency + GC%
sequence = "ATGCGATCGATCGTAGCGATCGATCGATGCGA"
freq = {}
for nt in sequence:
    freq[nt] = freq.get(nt, 0) + 1
gc_pct = (freq.get('G', 0) + freq.get('C', 0)) / len(sequence) * 100
```

**Goal:** Group genes by chromosome, then compare gene sets across studies or orthologous species.
**Approach:** `defaultdict(list)` removes the `if key not in d: d[key] = []` boilerplate; set operators (`&`, `|`, `-`, `^`) give one-line intersection/union/difference for gene-list comparisons.

```python
from collections import defaultdict

def group_by_chromosome(gene_locations):
    """Group (gene, chrom) pairs into {chrom: [genes]}."""
    by_chrom = defaultdict(list)
    for gene, chrom in gene_locations:
        by_chrom[chrom].append(gene)
    return by_chrom

gene_locations = [("BRCA1", "chr17"), ("TP53", "chr17"), ("EGFR", "chr7"), ("MYC", "chr8")]
by_chrom = group_by_chromosome(gene_locations)

## gene set algebra
cancer_genes = {"BRCA1", "TP53", "EGFR", "MYC", "KRAS"}
dna_repair_genes = {"BRCA1", "BRCA2", "ATM", "MLH1", "TP53"}

shared = cancer_genes & dna_repair_genes        # intersection
all_genes = cancer_genes | dna_repair_genes     # union
cancer_only = cancer_genes - dna_repair_genes   # difference
exclusive = cancer_genes ^ dna_repair_genes     # symmetric difference

## nested dict as a small annotation database
gene_db = {
    "BRCA1": {
        "chromosome": "17",
        "coordinates": (43044295, 43125483),  # GRCh37
        "strand": "-",
        "go_terms": ["DNA repair", "tumor suppression"],
    },
}
start, end = gene_db["BRCA1"]["coordinates"]
```

## Pitfalls

- **`KeyError` vs `get()`:** Direct access `d["NNN"]` raises `KeyError` on unknown codons. Use `GENETIC_CODE.get(codon, 'X')` to handle ambiguous bases.
- **Keys must be hashable:** Lists and dicts cannot be dict keys. Tuples like `("chr17", 43044295)` work; lists do not.
- **Iterating and modifying simultaneously:** Changing dict size during iteration raises `RuntimeError`. Collect changes separately, then apply.
- **`in` checks keys, not values:** `"ATG" in codon_table` is True if `"ATG"` is a key. For values: `"Met" in codon_table.values()`.
- **Sets are unordered:** `my_set[0]` raises `TypeError`. Use `sorted(my_set)` to get ordered elements.
- **Empty set:** `{}` creates an empty dict, not a set. Use `set()`.
- **Set elements must be hashable:** Use `frozenset` when you need a set of sets.
- **Counter arithmetic drops non-positive counts:** `Counter(a) - Counter(b)` discards zero/negative results, unlike a plain dict subtraction.

## See Also

- `bio-sequence-manipulation-codon-usage` — codon usage bias and codon-optimization tables.
- `bio-sequence-manipulation-transcription-translation` — Biopython `Seq.translate()` as an alternative to a manual codon dict.
- `bio-sequence-io-read-sequences` — loading real FASTA/GenBank records into sequences to feed these patterns.
- `bio-population-genetics-scikit-allel-analysis` — set/array operations at genome scale beyond in-memory Python dicts.

