Python Strings for Bioinformatics
When to Use
- Writing a quick reverse-complement, transcription, or codon-splitting function without pulling in Biopython.
- Computing GC content, nucleotide composition, or motif/codon counts from raw sequence strings.
- Parsing FASTA/UniProt headers (
>sp|P04637|P53_HUMAN ...) into accession/description fields. - Finding all (possibly overlapping) occurrences of a motif, or IUPAC-ambiguous patterns via regex.
- Debugging why a slice or
.count()gave the wrong answer against 1-based genomic coordinates (GFF/VCF).
Version Compatibility
Pure standard library — Python ≥3.8 (f-strings with =, str.maketrans/translate are stable since Python 3). No external packages required. For anything beyond ad-hoc string ops (parsing real FASTA files, alphabet validation, translation tables), switch to Bio.Seq/Bio.SeqIO from Biopython ≥1.78 — see python-bio-sequences.
Prerequisites
- Comfort with Python string slicing (
seq[start:stop:step]) and list comprehensions. - No packages to install — this is stdlib only (
refor regex patterns).
Core Patterns
Goal: compute a reverse complement, transcribe DNA→RNA, and split into codons correctly.
Approach: use str.maketrans + translate() for complementing (never chained replace() — see Pitfalls), then reverse with [::-1].
def reverse_complement(dna: str) -> str:
"""Return the reverse complement of a DNA string (uppercase A/T/G/C only)."""
complement_table = str.maketrans('ATGC', 'TACG')
return dna.upper().translate(complement_table)[::-1]
def transcribe(dna_coding_strand: str) -> str:
"""Transcribe a DNA coding strand to mRNA (T -> U)."""
return dna_coding_strand.upper().replace('T', 'U')
def gc_content(seq: str) -> float:
"""Fraction of G/C bases in seq, case-insensitive."""
seq = seq.upper()
return (seq.count('G') + seq.count('C')) / len(seq)
def codons(mrna: str) -> list[str]:
"""Split an mRNA/coding sequence into non-overlapping codons."""
return [mrna[i:i + 3] for i in range(0, len(mrna), 3)]
dna = "ATGCGATCGATCGTAG"
assert reverse_complement(dna) == "CTACGATCGATCGCAT"
assert transcribe(dna) == "AUGCGAUCGAUCGUAG"
assert round(gc_content(dna), 2) == 0.5
assert codons("AUGGCCGAUUAG") == ["AUG", "GCC", "GAU", "UAG"]
Goal: find all motif occurrences (including overlaps) and IUPAC-ambiguous patterns.
Approach: str.find() in a loop for overlapping literal matches; re.finditer for ambiguous/IUPAC motifs (e.g. TATA box TATA[AT]A[AT]).
import re
def find_all(seq: str, motif: str) -> list[int]:
"""Return all start positions of motif in seq, including overlapping matches."""
positions = []
pos = seq.find(motif)
while pos != -1:
positions.append(pos)
pos = seq.find(motif, pos + 1)
return positions
def find_tata_boxes(seq: str) -> list[tuple[int, str]]:
"""Find TATA-box-like elements (TATA[AT]A[AT]) and their start positions."""
return [(m.start(), m.group()) for m in re.finditer(r'TATA[AT]A[AT]', seq)]
assert find_all("AAAA", "AA") == [0, 1, 2] # overlapping: 3 hits
assert "AAAA".count("AA") == 2 # count() is non-overlapping
assert find_tata_boxes("NNTATAAAANN") == [(2, 'TATAAAA')]
Goal: parse a UniProt/NCBI-style FASTA header into its fields.
Approach: split on | for UniProt (db|accession|entry_name description), or on whitespace for simple >id description headers.
def parse_uniprot_header(header: str) -> dict:
"""Parse a '>sp|ACCESSION|ENTRY_NAME description' UniProt FASTA header."""
body = header.lstrip('>')
db, accession, rest = body.split('|', 2)
entry_name, _, description = rest.partition(' ')
return {"db": db, "accession": accession, "entry_name": entry_name, "description": description}
header = ">sp|P04637|P53_HUMAN Cellular tumor antigen p53 OS=Homo sapiens"
parsed = parse_uniprot_header(header)
assert parsed["accession"] == "P04637"
assert parsed["entry_name"] == "P53_HUMAN"
Sequence Features
seq = "ATGGCCGATTAGCCA"
seq.startswith('ATG') # has start codon
seq[-3:] in ('TAA', 'TAG', 'TGA') # ends with stop codon
'GATTAG' in seq # motif membership
all(c in 'ATGC' for c in seq.upper()) # validate DNA alphabet
Pitfalls
- Chained
replace()fails for complement:dna.replace('A','T').replace('T','A')converts all A→T then all T→A, including the ones just created — every base ends up A. Usestr.maketrans+translate(). find()returns -1, not None:if pos:is truthy for -1 (and falsy only for 0, a valid match at the start). Always checkif pos != -1:.count()is non-overlapping:"AAAA".count("AA")= 2, not 3. Use thefind()-loop pattern above for overlapping matches.- Case sensitivity:
.count('G')misses lowercase 'g' (common in soft-masked repeat sequences). Always.upper()first. - Strings are immutable:
seq[0] = 'C'raisesTypeError. Build new strings with slicing/concatenation,str.translate, or''.join(...). - Bioinformatics coordinates are 1-based, inclusive: GFF/VCF/SAM position 100 is Python index 99; a 1-based inclusive range
[start, end]becomes the Python sliceseq[start-1:end].
See Also
python-bio-sequences— BiopythonSeq/SeqRecordobjects for real FASTA/GenBank I/O and translation tables.python-bio-regular-expressions— regex patterns for IUPAC ambiguity codes, restriction sites, and complex motifs.python-bio-lists— list/slice mechanics that underlie codon splitting and k-mer windows.bio-core-motif-discovery— motif discovery and enrichment beyond literal/regex matching.