Biological Sequences as Python Strings
When to Use
- Writing a small sequence utility (reverse complement, GC%, codon split) without pulling in Biopython.
- Debugging a reverse-complement,
find(), or slicing bug that gives subtly wrong results.
- Parsing FASTA/FASTQ-like text by hand (headers, multi-line sequences) in a script or notebook.
- Converting between 0-based (Python/BED) and 1-based (VCF/GFF/SAM) coordinates.
- Scanning for motifs, restriction sites, or IUPAC degenerate patterns in raw sequence text.
Version Compatibility
Pure standard library — Python ≥3.8 (f-strings, str.maketrans/translate). No third-party dependencies. For anything beyond ad-hoc string ops (real FASTA/FASTQ I/O, alphabets, translation tables), switch to Biopython (see biopython skill).
Prerequisites
- Basic Python: strings, slicing, list comprehensions,
re module.
- Concepts: DNA/RNA/protein alphabets, codons, reading frames, 5'→3' orientation.
Core Operations
Goal: compute reverse complement, transcribe, and split into codons correctly.
Approach: use str.maketrans + translate for complement (never chained replace()), [::-1] to reverse, and step-3 slicing for codons.
def reverse_complement(dna: str) -> str:
"""Return the reverse complement of a DNA sequence (5'->3' input and output)."""
complement_table = str.maketrans('ATGC', 'TACG')
return dna.upper().translate(complement_table)[::-1]
def transcribe(dna_coding_strand: str) -> str:
"""Transcribe a DNA coding (sense) strand into mRNA (T -> U)."""
return dna_coding_strand.upper().replace('T', 'U')
def gc_content(seq: str) -> float:
"""Fraction of G/C bases in seq (0.0-1.0)."""
seq = seq.upper()
return (seq.count('G') + seq.count('C')) / len(seq)
def extract_codons(dna: str, reading_frame: int = 1) -> list[str]:
"""Split dna into complete codons for reading_frame 1, 2, or 3 (1-based frame)."""
dna = dna.upper()
start = reading_frame - 1
return [dna[i:i + 3] for i in range(start, len(dna) - 2, 3)]
Motif and Restriction-Site Scanning
Goal: find all (possibly overlapping) positions of a motif, including IUPAC degenerate patterns.
Approach: loop str.find() with start = pos + 1 for overlapping matches (count() is non-overlapping and undercounts); use re for degenerate IUPAC codes.
import re
def find_all(seq: str, motif: str) -> list[int]:
"""Return all 0-based start positions of motif in seq, including overlaps."""
seq, motif = seq.upper(), motif.upper()
positions = []
pos = seq.find(motif)
while pos != -1:
positions.append(pos)
pos = seq.find(motif, pos + 1)
return positions
def find_restriction_sites(sequence: str, site: str) -> list[int]:
"""1-based positions of a restriction site on the forward strand.
Callers should also scan reverse_complement(sequence) for palindromic
or antisense-strand sites (e.g. EcoRI GAATTC is palindromic).
"""
return [p + 1 for p in find_all(sequence, site)]
# IUPAC degenerate pattern example: TATA box (TATAWAW, W = A or T)
tata_hits = re.findall(r'TATA[AT]A[AT]', "GGGTATAAAAGGGTATATAT".upper())
FASTA Parsing (No Biopython)
Goal: parse a multi-record FASTA string/file into {id, description, sequence} records.
Approach: split on >, take the first whitespace-separated token of the header line as the id, join remaining lines as the sequence.
def parse_fasta(fasta_text: str) -> list[dict]:
"""Parse a multi-record FASTA string into a list of dicts with
'id', 'description', and 'sequence' keys."""
records = []
for entry in fasta_text.strip().split('>'):
if not entry.strip():
continue
lines = entry.strip().split('\n')
header_parts = lines[0].split(None, 1)
records.append({
'id': header_parts[0],
'description': header_parts[1] if len(header_parts) > 1 else '',
'sequence': ''.join(line.strip() for line in lines[1:]),
})
return records
def read_fasta_file(filepath: str) -> dict[str, str]:
"""Stream a FASTA file into {header: sequence} without loading it as one string."""
records = {}
with open(filepath) as f:
header, seq = None, []
for line in f:
line = line.strip()
if line.startswith('>'):
if header:
records[header] = ''.join(seq)
header, seq = line[1:], []
else:
seq.append(line)
if header:
records[header] = ''.join(seq)
return records
Coordinate Systems
| Format |
Base |
Interval type |
First 3 bp |
| Python |
0 |
Half-open [start, stop) |
seq[0:3] |
| BED |
0 |
Half-open |
start=0, end=3 |
| VCF/GFF |
1 |
Closed [start, stop] |
start=1, end=3 |
| SAM |
1 |
Closed |
POS=1 |
Converting GFF→Python: python_start = gff_start - 1 (stop stays the same for half-open slicing).
Pitfalls
- Chained
replace() for complement is wrong: dna.replace('A','T').replace('T','A') converts everything to A — use str.maketrans/translate which substitutes simultaneously.
find() returns -1 on miss, not None: if pos: is truthy for -1; always check if pos != -1:.
count() is non-overlapping: "ATATATAT".count("ATAT") is 2, not 3; use the find()-loop in find_all() above for overlaps.
- Case sensitivity: always
.upper() before scanning — lowercase denotes soft-masked repeats in UCSC/Ensembl output.
- Strings are immutable:
seq[0] = 'C' raises TypeError; build a new string via slicing/concatenation.
- Off-by-one from 1-based coordinates: subtract 1 when converting VCF/GFF/SAM positions to Python indices; forgetting this shifts every downstream slice by one base.
- Reading-frame math: frame N starts at index
N - 1, not N — extract_codons(dna, 2) starts at index 1.
See Also
biopython — real Seq/SeqRecord objects, alphabets, translation tables, and robust FASTA/FASTQ I/O for anything beyond quick scripting.
bio-sequence-io-read-sequences — file-based sequence I/O patterns (FASTA/FASTQ, compressed files).
bio-sequence-manipulation-reverse-complement — dedicated reverse-complement recipes and edge cases (ambiguity codes, RNA).
bio-restriction-analysis-restriction-sites — enzyme recognition-site databases and mapping beyond simple string search.
1---2name: python-bio-sequences3description: Manipulate DNA/RNA/protein sequences as raw Python strings: reverse complement via str.maketrans/translate, transcription, codon/ORF extraction, motif and restriction-site scanning with find()/re, and hand-rolled FASTA parsing without Biopython. Use when writing sequence utilities from scratch, debugging off-by-one slicing or 1-based-vs-0-based coordinate errors, or when Biopython/Seq is unavailable or overkill.4---56# Biological Sequences as Python Strings78## When to Use910- Writing a small sequence utility (reverse complement, GC%, codon split) without pulling in Biopython.11- Debugging a reverse-complement, `find()`, or slicing bug that gives subtly wrong results.12- Parsing FASTA/FASTQ-like text by hand (headers, multi-line sequences) in a script or notebook.13- Converting between 0-based (Python/BED) and 1-based (VCF/GFF/SAM) coordinates.14- Scanning for motifs, restriction sites, or IUPAC degenerate patterns in raw sequence text.1516## Version Compatibility1718Pure standard library — Python ≥3.8 (f-strings, `str.maketrans`/`translate`). No third-party dependencies. For anything beyond ad-hoc string ops (real FASTA/FASTQ I/O, alphabets, translation tables), switch to Biopython (see `biopython` skill).1920## Prerequisites2122- Basic Python: strings, slicing, list comprehensions, `re` module.23- Concepts: DNA/RNA/protein alphabets, codons, reading frames, 5'→3' orientation.2425## Core Operations2627**Goal:** compute reverse complement, transcribe, and split into codons correctly.28**Approach:** use `str.maketrans` + `translate` for complement (never chained `replace()`), `[::-1]` to reverse, and step-3 slicing for codons.2930```python31def reverse_complement(dna: str) -> str:32 """Return the reverse complement of a DNA sequence (5'->3' input and output)."""33 complement_table = str.maketrans('ATGC', 'TACG')34 return dna.upper().translate(complement_table)[::-1]3536def transcribe(dna_coding_strand: str) -> str:37 """Transcribe a DNA coding (sense) strand into mRNA (T -> U)."""38 return dna_coding_strand.upper().replace('T', 'U')3940def gc_content(seq: str) -> float:41 """Fraction of G/C bases in seq (0.0-1.0)."""42 seq = seq.upper()43 return (seq.count('G') + seq.count('C')) / len(seq)4445def extract_codons(dna: str, reading_frame: int = 1) -> list[str]:46 """Split dna into complete codons for reading_frame 1, 2, or 3 (1-based frame)."""47 dna = dna.upper()48 start = reading_frame - 149 return [dna[i:i + 3] for i in range(start, len(dna) - 2, 3)]50```5152## Motif and Restriction-Site Scanning5354**Goal:** find all (possibly overlapping) positions of a motif, including IUPAC degenerate patterns.55**Approach:** loop `str.find()` with `start = pos + 1` for overlapping matches (`count()` is non-overlapping and undercounts); use `re` for degenerate IUPAC codes.5657```python58import re5960def find_all(seq: str, motif: str) -> list[int]:61 """Return all 0-based start positions of motif in seq, including overlaps."""62 seq, motif = seq.upper(), motif.upper()63 positions = []64 pos = seq.find(motif)65 while pos != -1:66 positions.append(pos)67 pos = seq.find(motif, pos + 1)68 return positions6970def find_restriction_sites(sequence: str, site: str) -> list[int]:71 """1-based positions of a restriction site on the forward strand.72 Callers should also scan reverse_complement(sequence) for palindromic73 or antisense-strand sites (e.g. EcoRI GAATTC is palindromic).74 """75 return [p + 1 for p in find_all(sequence, site)]7677# IUPAC degenerate pattern example: TATA box (TATAWAW, W = A or T)78tata_hits = re.findall(r'TATA[AT]A[AT]', "GGGTATAAAAGGGTATATAT".upper())79```8081## FASTA Parsing (No Biopython)8283**Goal:** parse a multi-record FASTA string/file into `{id, description, sequence}` records.84**Approach:** split on `>`, take the first whitespace-separated token of the header line as the id, join remaining lines as the sequence.8586```python87def parse_fasta(fasta_text: str) -> list[dict]:88 """Parse a multi-record FASTA string into a list of dicts with89 'id', 'description', and 'sequence' keys."""90 records = []91 for entry in fasta_text.strip().split('>'):92 if not entry.strip():93 continue94 lines = entry.strip().split('\n')95 header_parts = lines[0].split(None, 1)96 records.append({97 'id': header_parts[0],98 'description': header_parts[1] if len(header_parts) > 1 else '',99 'sequence': ''.join(line.strip() for line in lines[1:]),100 })101 return records102103def read_fasta_file(filepath: str) -> dict[str, str]:104 """Stream a FASTA file into {header: sequence} without loading it as one string."""105 records = {}106 with open(filepath) as f:107 header, seq = None, []108 for line in f:109 line = line.strip()110 if line.startswith('>'):111 if header:112 records[header] = ''.join(seq)113 header, seq = line[1:], []114 else:115 seq.append(line)116 if header:117 records[header] = ''.join(seq)118 return records119```120121## Coordinate Systems122123| Format | Base | Interval type | First 3 bp |124|--------|------|--------------|------------|125| Python | 0 | Half-open `[start, stop)` | `seq[0:3]` |126| BED | 0 | Half-open | `start=0, end=3` |127| VCF/GFF | 1 | Closed `[start, stop]` | `start=1, end=3` |128| SAM | 1 | Closed | `POS=1` |129130Converting GFF→Python: `python_start = gff_start - 1` (stop stays the same for half-open slicing).131132## Pitfalls133134- **Chained `replace()` for complement is wrong**: `dna.replace('A','T').replace('T','A')` converts everything to `A` — use `str.maketrans`/`translate` which substitutes simultaneously.135- **`find()` returns `-1` on miss, not `None`**: `if pos:` is truthy for `-1`; always check `if pos != -1:`.136- **`count()` is non-overlapping**: `"ATATATAT".count("ATAT")` is `2`, not 3; use the `find()`-loop in `find_all()` above for overlaps.137- **Case sensitivity**: always `.upper()` before scanning — lowercase denotes soft-masked repeats in UCSC/Ensembl output.138- **Strings are immutable**: `seq[0] = 'C'` raises `TypeError`; build a new string via slicing/concatenation.139- **Off-by-one from 1-based coordinates**: subtract 1 when converting VCF/GFF/SAM positions to Python indices; forgetting this shifts every downstream slice by one base.140- **Reading-frame math**: frame *N* starts at index `N - 1`, not `N` — `extract_codons(dna, 2)` starts at index 1.141142## See Also143144- `biopython` — real `Seq`/`SeqRecord` objects, alphabets, translation tables, and robust FASTA/FASTQ I/O for anything beyond quick scripting.145- `bio-sequence-io-read-sequences` — file-based sequence I/O patterns (FASTA/FASTQ, compressed files).146- `bio-sequence-manipulation-reverse-complement` — dedicated reverse-complement recipes and edge cases (ambiguity codes, RNA).147- `bio-restriction-analysis-restriction-sites` — enzyme recognition-site databases and mapping beyond simple string search.