| name | python-bio-tuples |
| description | 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. |
| tool_type | python |
| primary_tool | Python |
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]).
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.
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.
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:
_seen = set()
updated = cds_list[:]
updated.append(codon)
_seen.add(codon)
return updated
original = ["ATG", "GCC", "GAT"]
extended = add_codon(original, "TAG")
assert original == ["ATG", "GCC", "GAT"]
assert extended == ["ATG", "GCC", "GAT", "TAG"]
exon_sets = [["ATG", "GCC"], ["GAT", "TAG"]]
shallow = exon_sets.copy()
deep = copy.deepcopy(exon_sets)
shallow[0].append("XXX")
assert exon_sets[0] == ["ATG", "GCC", "XXX"]
assert deep[0] == [, ]
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.