# Python Bio Tuples

> Build immutable Python tuple/namedtuple records for gene coordinates and SNP tuples (chrom,pos,ref,alt). Use when storing fixed records, returning multiple values, using coords as dict keys, or fixing list-aliasing/mutable-default bugs.

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

---


# Tuples and Immutable Records in Bioinformatics

## When to Use

- Representing a fixed genomic record — `(gene_name, chromosome, start, end, strand)` or a VCF-style variant `(chrom, pos, ref, alt)`.
- Returning multiple values from a function (e.g. `find_overlap()` returning `(start, end, length)`).
- Using genomic coordinates as dictionary keys (tuples are hashable, lists are not).
- Making a record read-only so downstream code can't accidentally mutate a coordinate or reference allele.
- Debugging a bug where modifying one list unexpectedly changed another (aliasing), or a function's results grow across calls (mutable default argument).

## Version Compatibility

Pure Python stdlib — no version-sensitive APIs. Works unchanged on Python ≥3.8. `collections.namedtuple` and `typing.NamedTuple` have been stable since Python 3.6+; no external packages required.

## Prerequisites

- Basic Python: variables, functions, string slicing, f-strings.
- Genomic coordinate conventions (0-based half-open BED vs 1-based GFF/VCF) — see `bio-genome-intervals-bed-file-basics`.
- `python-bio-lists` for the mutable-collection counterpart to the patterns here.

## Core Patterns

**Goal:** parse a fixed-width or delimited genomic record line into an immutable, self-documenting record instead of a raw tuple of positional fields.
**Approach:** define a `namedtuple` once, then build instances from parsed text; access fields by name (`gene.start`) instead of brittle indices (`fields[2]`).

```python
from collections import namedtuple

Gene = namedtuple("Gene", ["name", "chromosome", "start", "end", "strand"])


def parse_gene_line(line):
    """Parse a tab-separated 'name chrom start end strand' line into a Gene record.

    Parameters:
        line (str): e.g. "BRCA1\tchr17\t43044295\t43125483\t-"

    Returns:
        Gene: immutable namedtuple record
    """
    name, chrom, start, end, strand = line.strip().split("\t")
    return Gene(name, chrom, int(start), int(end), strand)


genes = [
    parse_gene_line("BRCA1\tchr17\t43044295\t43125483\t-"),
    parse_gene_line("TP53\tchr17\t7661779\t7687538\t-"),
    parse_gene_line("EGFR\tchr7\t55019017\t55207337\t+"),
]

by_length = sorted(genes, key=lambda g: g.end - g.start, reverse=True)
for g in by_length:
    print(f"{g.name:6s} {g.chromosome}:{g.start}-{g.end}  ({g.end - g.start:,} bp)")
```

**Goal:** use variant coordinates as dictionary keys so lookups and duplicate-detection are O(1) instead of scanning a list.
**Approach:** a VCF record's `(chrom, pos, ref, alt)` is a natural hashable tuple key — lists cannot be used this way because they are unhashable.

```python
def annotate_variants(calls, known_pathogenic):
    """Flag which (chrom, pos, ref, alt) variant calls are in a known-pathogenic set.

    Parameters:
        calls (list[tuple]): [(chrom, pos, ref, alt), ...] from a VCF
        known_pathogenic (set[tuple]): set of (chrom, pos, ref, alt) tuples

    Returns:
        dict: {variant_tuple: is_pathogenic (bool)}
    """
    return {call: call in known_pathogenic for call in calls}


calls = [
    ("chr7", 55259515, "T", "G"),
    ("chr17", 43044295, "G", "A"),
]
known_pathogenic = {("chr7", 55259515, "T", "G")}

result = annotate_variants(calls, known_pathogenic)
for variant, is_path in result.items():
    print(f"{variant}: {'PATHOGENIC' if is_path else 'benign/unknown'}")
```

**Goal:** avoid two classic aliasing bugs — sharing a list across variables, and a mutable default argument that leaks state between calls.
**Approach:** copy lists explicitly (`[:]` or `.copy()`, `copy.deepcopy()` for nested structures); default to `None` and build the mutable object inside the function body.

```python
import copy


def add_codon(cds_list, codon, _seen=None):
    """Append a codon to an independent copy of cds_list (no aliasing, no shared default).

    Parameters:
        cds_list (list[str]): existing codons
        codon (str): codon to add
        _seen (set, optional): internal dedup set; never share a mutable default

    Returns:
        list[str]: a NEW list with codon appended (original untouched)
    """
    if _seen is None:          # correct pattern: never `def f(_seen=set())`
        _seen = set()
    updated = cds_list[:]      # shallow copy -- original list is untouched
    updated.append(codon)
    _seen.add(codon)
    return updated


original = ["ATG", "GCC", "GAT"]
extended = add_codon(original, "TAG")
assert original == ["ATG", "GCC", "GAT"]      # unchanged
assert extended == ["ATG", "GCC", "GAT", "TAG"]

# Nested structures need deepcopy: a shallow copy still shares inner lists
exon_sets = [["ATG", "GCC"], ["GAT", "TAG"]]
shallow = exon_sets.copy()
deep = copy.deepcopy(exon_sets)
shallow[0].append("XXX")       # mutates exon_sets[0] too (shared inner list)
assert exon_sets[0] == ["ATG", "GCC", "XXX"]
assert deep[0] == ["ATG", "GCC"]              # deep copy is fully independent
```

## Pitfalls

- **Assignment does not copy:** `alias = original` makes both names point to the same list; use `original[:]` or `original.copy()` for an independent shallow copy.
- **Shallow copy ≠ deep copy:** `list.copy()` only copies the top level — nested lists/dicts are still shared; use `copy.deepcopy()`.
- **Tuples are immutable:** cannot append, remove, or reassign an element (`snp[2] = "C"` raises `TypeError`); rebuild via slicing/concatenation instead: `snp[:2] + ("C",) + snp[3:]`.
- **Single-element tuples need a trailing comma:** `("BRCA1",)` is a tuple; `("BRCA1")` is just a string.
- **Mutable default arguments:** `def f(x=[])` reuses the same list across every call with no argument — use `def f(x=None)` and create the object inside the function.
- **Lists can't be dict keys or set members** (unhashable); tuples can — this is why VCF-style `(chrom, pos, ref, alt)` records are typically tuples, not lists.
- **Off-by-one on genomic coordinates:** Python slicing is half-open `[start, stop)`; BED is 0-based half-open, GFF/VCF are 1-based inclusive — mixing them silently shifts coordinates by one.

## See Also

- `python-bio-lists` — mutable list operations, sorting, and codon/k-mer extraction that pairs with these tuple patterns.
- `python-bio-dictionaries` — using tuple keys in dicts/sets for variant and coordinate lookups.
- `bio-genome-intervals-bed-file-basics` — BED coordinate conventions referenced above.
- `bio-variant-calling-vcf-basics` — VCF record fields behind the `(chrom, pos, ref, alt)` tuple pattern.

