Biopython11/15
Starting sandbox…
Beginner·4 graded tests

Seq

Seq

Core Principle: The Bio.Seq.Seq class provides an immutable sequence container equipped with domain-specific biological transformations, including reverse complementation, transcription, and translation.

Creating and using

[1]python
out1ms
Seq('ATGGCCATTGTAATGGGCCGC')

Seq supports nearly the whole str interface, so most string intuition transfers directly.

Bio.Alphabet is gone

Removed in Biopython 1.78. This is a TypeError now:

from Bio.Alphabet import IUPAC          # ImportError
Seq("ATG", IUPAC.unambiguous_dna)        # TypeError

A Seq is just letters. The library does not know whether ATG is DNA or protein, and mostly does not need to. Where a format requires it (writing GenBank), you supply it as metadata:

record.annotations["molecule_type"] = "DNA"

Every tutorial written before 2020 will show the alphabet API. See Biopython Deprecations.

The biology methods

[2]python
out1ms
Seq('GCGGCCCAUUACAAUGGCCAU')

complement() vs reverse_complement(): complement alone gives you the opposite strand read in the same direction, which is almost never biologically meaningful. For "the other strand's sequence as it would be read", you want reverse_complement().

Translation, carefully

[3]python
error1ms
Traceback (most recent call last):
  File "/Users/leviosman/Developer/python-for-bio/webapp/scripts/curriculum/generate_cell_outputs.py", line 126, in run_cell_capture
    exec(compile(head, "<cell>", "exec"), namespace)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<cell>", line 4, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/Bio/Seq.py", line 1617, in translate
    _translate_str(str(self), table, stop_symbol, to_stop, cds, gap=gap)
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/Bio/Seq.py", line 2866, in _translate_str
    raise CodonTable.TranslationError(
        f"Final codon '{sequence[-3:]}' is not a stop codon"
    )
Bio.Data.CodonTable.TranslationError: Final codon 'CGC' is not a stop codon

Points to internalise:

  • to_stop=True is usually what you mean. Without it, translation runs to the end of the sequence and you get MAIVMGR*KGAR* with internal stops.
  • cds=True validates: it requires a length divisible by 3, a valid start codon, a single terminal stop, and no internal stops. It raises TranslationError otherwise. Use it when you believe you have a real CDS: the exception is information.
  • Partial codons warn (BiopythonWarning: Partial codon) and are dropped.
  • Table matters. Mitochondrial genomes use different codon tables; TGA is a stop in the standard code and tryptophan in vertebrate mitochondria. Getting this wrong silently truncates your protein.
[4]python
out1ms
Table 2 Vertebrate Mitochondrial, SGC1

  |  T      |  C      |  A      |  G      |
--+---------+---------+---------+---------+--
T | TTT F   | TCT S   | TAT Y   | TGT C   | T
T | TTC F   | TCC S   | TAC Y   | TGC C   | C
T | TTA L   | TCA S   | TAA Stop| TGA W   | A
T | TTG L   | TCG S   | TAG Stop| TGG W   | G
--+---------+---------+---------+---------+--
C | CTT L   | CCT P   | CAT H   | CGT R   | T
C | CTC L   | CCC P   | CAC H   | CGC R   | C
C | CTA L   | CCA P   | CAA Q   | CGA R   | A
C | CTG L   | CCG P   | CAG Q   | CGG R   | G
--+---------+---------+---------+---------+--
A | ATT I(s)| ACT T   | AAT N   | AGT S   | T
A | ATC I(s)| ACC T   | AAC N   | AGC S   | C
A | ATA M(s)| ACA T   | AAA K   | AGA Stop| A
A | ATG M(s)| ACG T   | AAG K   | AGG Stop| G
--+---------+---------+---------+---------+--
G | GTT V   | GCT A   | GAT D   | GGT G   | T
G | GTC V   | GCC A   | GAC D   | GGC G   | C
G | GTA V   | GCA A   | GAA E   | GGA G   | A
G | GTG V(s)| GCG A   | GAG E   | GGG G   | G
--+---------+---------+---------+---------+--

Immutability

[5]python
error1ms
Traceback (most recent call last):
  File "/Users/leviosman/Developer/python-for-bio/webapp/scripts/curriculum/generate_cell_outputs.py", line 132, in run_cell_capture
    exec(compile(code, "<cell>", "exec"), namespace)
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<cell>", line 1, in <module>
TypeError: 'Seq' object does not support item assignment

Immutability means Seq is hashable and safe to share. Use MutableSeq for in-place editing, then convert back.

For simulating a substitution the string route is usually clearer:

mutated = Seq(str(s)[:pos] + alt + str(s)[pos + len(ref):])

Undefined and partial sequences

[6]python
error1ms
Traceback (most recent call last):
  File "/Users/leviosman/Developer/python-for-bio/webapp/scripts/curriculum/generate_cell_outputs.py", line 128, in run_cell_capture
    value = eval(compile(tail, "<cell>", "eval"), namespace)
  File "<cell>", line 4, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/Bio/Seq.py", line 407, in __str__
    return self._data.decode("ASCII")
           ~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/Bio/Seq.py", line 155, in decode
    return bytes(self).decode(encoding)
           ~~~~~^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/site-packages/Bio/Seq.py", line 2387, in __bytes__
    raise UndefinedSequenceError("Sequence content is undefined")
Bio.Seq.UndefinedSequenceError: Sequence content is undefined

This is how Biopython represents a record whose length is known from a header but whose sequence was not loaded (common with GenBank contig records and some SeqIO indexing modes). Guard with a try/except if you parse arbitrary files.

Bioinformatics examples

from Bio.Seq import Seq
from Bio.SeqUtils import gc_fraction, molecular_weight

s = Seq("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG")

gc_fraction(s)                             # 0.0–1.0 (NOT the removed GC(), 0–100)
molecular_weight(s, seq_type="DNA")
s.translate(to_stop=True)                  # MAIVMGR

# find ORFs in all six frames
def orfs(seq, min_aa=50):
    for strand, nuc in [(+1, seq), (-1, seq.reverse_complement())]:
        for frame in range(3):
            trimmed = nuc[frame: len(nuc) - (len(nuc) - frame) % 3]
            for prot in trimmed.translate().split("*"):
                if len(prot) >= min_aa:
                    yield strand, frame, prot

# reverse-strand variant: get the sequence as the transcript reads it
if strand == "-":
    coding = genomic_seq.reverse_complement()

# codon usage
codons = [str(s[i:i+3]) for i in range(0, len(s) - 2, 3)]
pd.Series(codons).value_counts()

# GC at the third codon position (GC3): a codon-bias measure
gc3 = sum(1 for i in range(2, len(s), 3) if s[i] in "GC") / (len(s) // 3)

Ambiguity codes

IUPAC ambiguity letters (N, R, Y, W, S, K, M, ...) are handled correctly by complement/reverse_complement: R (A or G) complements to Y (C or T). Translation of an ambiguous codon gives X where the amino acid is undetermined, or the specific residue where all possibilities agree.

Real-world sequences contain N (unknown base) and soft-masked lowercase (repeat-masked regions). s.upper() before comparisons, and decide deliberately whether N counts in your GC denominator: gc_fraction has an ambiguous= parameter for this.

Common mistakes

  • Using Bio.Alphabet. Removed.
  • ungap(), tomutable(), toseq(). Removed. Use .replace("-", "") and the constructors.
  • translate() without to_stop=True, getting internal stops.
  • Wrong codon table for mitochondrial or bacterial sequences.
  • complement() where you meant reverse_complement().
  • Forgetting reverse-strand genes need reverse-complementing before translation.
  • Assuming Seq is mutable.
  • Ignoring soft-masking. Lowercase bases fail a naive == "G" test.
  • SeqUtils.GC(): removed in 1.82, and it returned a percentage while gc_fraction returns a fraction. Off by 100×.

See also

SeqRecord · SeqIO · Bio.SeqUtils · Biopython Deprecations · Applied - Sequence Composition Analysis · Applied - FASTA and FASTQ Workflows

Exercise

Biopython is not installed here, so build the standard codon table yourself and translate with it. The table is generated from the base order TCAG: iterate the first, second and third position in that order through all 64 combinations, and read off the amino acids in the same order from

FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG

Write translate_report(raw) returning three strings. Upper-case the input first, because soft-masked lowercase bases are common and a naive lookup would miss every one of them. Translate frame 0 only, dropping a trailing partial codon rather than padding it, and rendering any codon containing a character other than A, C, G or T as X. Return the full translation with * for stop codons, then the translation truncated before the first stop, then the same truncated translation of the reverse complement. Print them as full=, to_stop= and rc_to_stop=. Complement A to T, C to G and their inverses, and map anything else to N. A sequence shorter than three bases yields three empty values, and the three lines must still be printed.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdinATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG full=MAIVMGR*KGAR* to_stop=MAIVMGR rc_to_stop=LSGTLSAAHYNGH
2stdinatggaggagccgcagtcagatcctagcgtcgagccccctctgagtcaggaaacattttca full=MEEPQSDPSVEPPLSQETFS to_stop=MEEPQSDPSVEPPLSQETFS rc_to_stop=
3stdinATGNNNCATTAAGGGCC full=MXH*G to_stop=MXH rc_to_stop=GP
4stdinAT full= to_stop= rc_to_stop=

Hints · 3

01Hint

Three nested loops over "TCAG" and an index into the amino-acid string build all 64 entries.

02Hint

table.get(codon, "X") handles ambiguous codons without a separate test.

03Hint

Truncating at the first stop is protein.split("*")[0].