# Biopython Seq Objects Explained

A Biopython Seq object is a class from the `Bio.Seq` module that stores a biological sequence, such as DNA, RNA, or a protein, and exposes sequence-aware operations like reverse complementation, transcription, and translation. It behaves like a Python string for most everyday purposes but carries methods and semantics that only make sense for biological sequence data.

This matters because nearly every Python for bioinformatics workflow starts with a sequence in memory. Whether you are filtering contigs by GC content, translating an open reading frame, or preparing a FASTA file for downstream alignment, the Seq object is the container that makes those steps a method call instead of a hand-rolled function. Python has a long track record as an implementation language for sequence tooling, from early segmentation modules such as Sarment [1] through modern libraries for population genetics such as EggLib [2], and Biopython's Seq object is the common denominator that most of these pipelines pass around. Several Biopython submodules, including the interface to the SCOP and ASTRAL structural classification resources, were written specifically to work as part of the Biopython distribution [3], which tells you how central this library is to the ecosystem.

## What Is the Biopython Seq Object?

The Seq object is a lightweight wrapper around a string of sequence characters. When you write `Seq("AGTACACTGGT")`, you get an object that:

- Stores the sequence text exactly as you provided it, including ambiguous IUPAC codes such as N, R, and Y.
- Answers to `len()`, slicing, iteration, and concatenation the way a string does.
- Adds biological methods a string lacks, such as `.reverse_complement()`, `.transcribe()`, `.back_transcribe()`, and `.translate()`.
- Is immutable. Every operation that transforms the sequence returns a new Seq object and leaves the original untouched.

The design intent is that you should be able to drop a Seq object almost anywhere a string would work, and get biologically meaningful behavior in return. A plain string cannot tell you what `ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG` encodes. A Seq object can tell you it is 36 nucleotides long, that its reverse complement is `CTATCGGGCACCCTTTCAGCGCCCATTCACAATGGCCAT`, and that it translates to the peptide `MAIVMGR*KGAR*` under the standard genetic code.

One historical note worth knowing before you read older material. For many years Biopython shipped an "Alphabet" system that attached a declared molecule type to each Seq object. Older biopython tutorial pages show code like `Seq("AGTACACTGGT", IUPAC.unambiguous_dna)`. That system was deprecated and then removed in the 2020 to 2021 release series, so any tutorial that passes an alphabet argument will fail on a current installation. The modern Seq object infers what it can at call time, for example by letting you pass an explicit codon table to `translate()`, rather than relying on a declared alphabet.

## Why Python for Bioinformatics?

The Seq object does not exist in a vacuum. Python became a dominant language in this field because it is readable, scriptable, and easy to glue onto command line tools, and a large body of published software confirms that pattern:

- The Galaxy project implemented its full tool suite for manipulating FASTQ variants in Python and integrated it into the Galaxy analysis platform [4].
- EggLib pairs a fast C++ core with a high level Python interface for population genetic analyses such as coalescent simulations and neutrality tests [2].
- ETE provides automated manipulation, analysis, and visualization of phylogenetic and clustering trees from Python [5].
- Plastid represents sequencing data as arrays of values tied to genomic or transcriptomic positions for nucleotide-resolution analysis [6].
- The hgvs package parses, validates, maps, and formats sequence variants using [Human Genome](/blog/guides/human-genome) Variation Society nomenclature [7], and the VariantValidator web service is built on top of that same Python package [8].

The Seq object sits at the bottom of this stack. Larger packages either use Biopython directly or reimplement similar ideas, so understanding Seq first makes every later tool easier to read.

## Creating Your First Seq Object

Install Biopython with your package manager:

```bash
pip install biopython
```

Then open the Python interpreter and follow this minimal biopython seq object tutorial example:

```python
from Bio.Seq import Seq

my_seq = Seq("AGTACACTGGT")
my_seq
```

Output:

```
Seq('AGTACACTGGT')
```

Everyday operations work exactly as a string intuition predicts:

```python
len(my_seq)          # 11
my_seq            # 'A'
my_seq[0:3]          # Seq('AGT')
my_seq[::-1]         # Seq('TGGTCACATGA'), the reverse
for base in my_seq:  # iterates one character at a time
    print(base)
```

You can also chain operations, because every method returns a Seq:

```python
my_seq.upper().reverse_complement()
```

One frequent question at this point is how to get the raw text back for output files, regular expressions, or third-party libraries. Convert explicitly:

```python
str(my_seq)          # 'AGTACACTGGT'
```

Relying on explicit conversion instead of implicit behavior keeps your code portable across Biopython versions, because the details of how Seq compares and prints have shifted slightly over the library's history.

## Core Methods Every Workflow Uses

### Complement and Reverse Complement

```python
my_seq.complement()          # Seq('TCATGTGACCA')
my_seq.reverse_complement()  # Seq('ACCAGTGTACT')
```

The distinction is a classic source of errors. A complement swaps each base for its pairing partner but keeps the left to right order. A reverse complement does both, which is what you need when you want the sequence of the opposite strand read in the standard 5 prime to 3 prime direction. Primer design, antisense probe design, and double stranded feature extraction almost always want the reverse complement, not the bare complement. Both methods handle ambiguous IUPAC codes, so an N stays an N, and R (a purine) stays R.

### Transcription and Back Transcription

```python
coding_dna = Seq("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG")
coding_dna.transcribe()      # Seq('AUGGCCAUUGUAAGGGUGCCCGAUAG') style, T becomes U
```

Precisely, `transcribe()` replaces every T with U, giving `AUGGCCAUUGUAUGGGCCGCTGAAAGGGTGCCCGAUAG` for the sequence above. The `back_transcribe()` method does the reverse, turning U back into T. Note the semantic convention: these methods treat your input as the coding strand, the strand with the same sequence as the RNA. If you start from the template strand instead, transcribe the reverse complement.

### Translation and Codon Tables

```python
coding_dna = Seq("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG")
coding_dna.translate()
# Seq('MAIVMGR*KGAR*')
```

Three points about `translate()`:

1. Stop codons become the asterisk character. The default translation runs through the entire sequence and does not stop at the first stop codon.
2. If you want the protein up to the first in-frame stop codon, pass `to_stop=True`. If you are translating a confirmed coding sequence, `cds=True` checks that the sequence starts with a valid start codon, has no internal stop codons, and ends at a stop codon.
3. Non-standard genetic codes are selected by NCBI table number. Bacterial coding sequences typically use `table=11`, and vertebrate mitochondrial sequences use `table=2`. Under table 2, the codon AGA signals stop rather than arginine, so table selection changes output in real ways.

```python
coding_dna.translate(to_stop=True)     # Seq('MAIVMGR')
coding_dna.translate(table=2)          # different residues at AGA positions
```

### Counting and GC Content

```python
my_seq.count("A")               # 3
my_seq.count("GG")              # 1, non-overlapping by default
my_seq.count("GG", overlap=1)   # overlapping matches
```

For GC content, current Biopython versions provide `gc_fraction` in `Bio.SeqUtils`:

```python
from Bio.SeqUtils import gc_fraction

gc_fraction(my_seq)        # returns a fraction, here 0.5454...
gc_fraction(my_seq) * 100  # 54.54 percent
```

Older tutorials use `Bio.SeqUtils.GC`, which returned a percentage directly. If you copy code from an old biopython tutorial and the import fails, this is usually why. Multiply the fraction by 100 when you need a percentage, and state your units in any report or script output.

### Ambiguous and Incomplete Data

Real sequencing data contains ambiguity. A Seq object stores whatever characters you give it, including N, and translation handles ambiguity as far as the genetic code allows. A codon that cannot be resolved to a single amino acid, such as one containing N in a position that changes the residue, translates to X. This behavior is deliberate. Silent propagation of X through a translation is easier to audit than an exception buried inside a loop over ten thousand contigs.

## From Seq to SeqRecord and SeqIO: Reading Real Files

![Four step flow showing a FASTA file parsed by SeqIO, the Seq object extracted, translated, and written back out as a protein FASTA.](/article-images/illustrations/biopython-seq-object-tutorial-seq-to-protein.webp)

A Seq object holds one sequence and nothing else. Real files attach identifiers, descriptions, quality scores, and annotations, and Biopython wraps Seq in the SeqRecord class for that purpose. The `SeqIO` module reads and writes standard formats into SeqRecord objects, which is how almost every real workflow begins:

```python
from Bio import SeqIO

for record in SeqIO.parse("example.fasta", "fasta"):
    print(record.id)          # the FASTA identifier
    print(record.description) # the full header line
    print(len(record.seq))    # the Seq object lives in record.seq
    print(record.seq[:30])    # slicing works right off the record
```

The same parser interface covers GenBank, EMBL, FASTQ, and other formats, so switching input format is a one-string change:

```python
for record in SeqIO.parse("reads.fastq", "fastq"):
    # record.seq is the bases, record.letter_annotations holds quality
    pass
```

FASTQ handling deserves a note. Quality filtering of [next generation sequencing](/blog/guides/next-generation-sequencing) reads is a well established workflow step, and tool suites that implement it across the known [FASTQ format](/knowledge/bioinformatics/fastq-file-format-phred-scores-qc) variants were implemented in Python for the Galaxy platform [4]. When your data moves from raw reads to assembled contigs, SeqIO keeps the same object model in place, which is the main reason pipelines in this ecosystem read cleanly from end to end.

Writing files back out is symmetric:

```python
SeqIO.write(records, "output.fasta", "fasta")
```

## A Complete Worked Example: GC Filtering and Translation

The following script, suitable as the skeleton of a biopython seq object tutorial exercise, reads a FASTA file of contigs, keeps those with GC content between 40 and 60 percent, translates each to the first stop codon, and keeps the record only if the predicted peptide is at least 50 amino acids long:

```python
from Bio import SeqIO
from Bio.SeqUtils import gc_fraction

kept = []

for record in SeqIO.parse("contigs.fasta", "fasta"):
    gc_percent = gc_fraction(record.seq) * 100
    if 40 <= gc_percent <= 60:
        protein = record.seq.translate(to_stop=True)
        if len(protein) >= 50:
            kept.append(record)
            print(record.id, gc_percent, len(protein))

SeqIO.write(kept, "filtered_contigs.fasta", "fasta")
print(f"Kept {len(kept)} records")
```

Three workflow habits are visible in this short script. First, the Seq object does the biology (`translate`, and `gc_fraction` operating on sequence composition) while SeqIO does the file plumbing. Second, filtering conditions read almost like the sentence describing them, which is the practical payoff of string-like behavior. Third, every transform produces a new object, so the original record in the parsed list remains unmodified even after you translate its sequence.

## Choosing the Right Object: Seq, MutableSeq, SeqRecord, and str

| Object | What it holds | Mutable | Biological methods | Typical use |
|, - |, - |, - |, - |, - |
| `str` | Plain text | Yes | None | Generic text, regex, output formatting |
| `Seq` | One sequence | No | Yes, full set | Single sequences, transformations, translation |
| `MutableSeq` | One editable sequence | Yes | Yes | In-place editing, construction by accumulation |
| `SeqRecord` | A Seq plus id, name, description, annotations, and quality data | Sequence is immutable by default | Delegates to its `.seq` | File input and output via SeqIO |

The most common confusion in any biopython seq object tutorial explained discussion is Seq versus SeqRecord. The rule is simple. If you need the sequence and nothing else, use Seq. If the sequence came from or is going to a file, use SeqRecord, because identifiers and annotations travel with it. If you find yourself building a sequence by repeated concatenation and the copies bother you, switch to MutableSeq:

```python
from Bio.Seq import MutableSeq

editable = MutableSeq("AGTACACTGGT")
editable[7] = "T"
editable.reverse_complement(inplace=True)
```

Because Seq is immutable, `seq += Seq("AAAA")` inside a loop creates a new object every iteration. For a handful of operations this cost is irrelevant, and for a hot loop MutableSeq or a list of characters joined at the end is the better pattern.

## Testing and Observing Seq Objects in Practice

Sequence code fails quietly when untested, because a wrong reverse complement still produces a plausible looking string. Three practices catch most of these failures.

Round trip tests exploit the fact that most Seq transforms are invertible:

```python
def test_reverse_complement_round_trip():
    seq = Seq("ACGTNRYSW")
    assert seq.reverse_complement().reverse_complement() == seq

def test_translate_length():
    seq = Seq("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG")
    assert len(seq) % 3 == 0
```

Invariants like "the reverse complement of the reverse complement is the original sequence" hold for every input including ambiguous codes, and they run in milliseconds. Run them with a standard runner:

```bash
pytest test_seq_tools.py
```

The interactive interpreter is the other observation tool. Because Seq objects print their contents, you can paste a sequence, call a method, and see the result immediately, which makes them ideal for checking an assumption before embedding it in a script. Keep a small known example, like the 36 nucleotide coding sequence above with its known translation, in your test file. When a Biopython upgrade changes behavior, that one known-good example will tell you immediately.

## Where Seq Fits in Larger Pipelines

The Seq object is the leaf node of most genomics pipelines, and the wider Python ecosystem shows how it connects upward. For phylogenomic studies, PHYLUCE assembles contigs from enriched sequencing data, identifies targeted loci, and prepares alignments across hundreds of taxa [9], with sequence handling at the base of that chain. For chromatin and transcriptome work, metaseq loads multiple genomic data formats into standard Python structures for integrative analysis [10], and PyGMQL scales region based queries over thousands of heterogeneous experiments on distributed infrastructure [11]. For annotation-driven work, tools such as GAD extract features from GFF3 and GTF files into formats like BED and FASTA [12], and ProdMX analyzes protein functional domains across millions of genomes using compressed sparse matrices [13]. For structural biology benchmarking, Biopython itself hosts the Python interface to SCOP and ASTRAL for building domain datasets [3]. For clinical interpretation, the hgvs package parses and validates variant descriptions against Human Genome Variation Society recommendations [7], and VariantValidator layers a web interface on it that corrects common nomenclature mistakes and interconverts HGVS and Variant Call Format descriptions [8].

The pattern across all of these is the same. Specialized libraries handle their domain, and a plain, well-defined sequence representation sits underneath. That is exactly the niche the Seq object fills, and it is why time spent on this foundation pays off across the whole toolkit.

## Common Misconceptions

**A Seq object is just a string with extra methods.** The string-like behavior is real, but the semantics differ. Equality comparison between a Seq and a plain string has varied across Biopython versions, so code that mixed the two types could pass on one installation and fail on another. Convert with `str()` when you need a guaranteed string.

**`translate()` stops at the first stop codon.** It does not by default. It translates the entire sequence and marks stop codons with asterisks. `to_stop=True` produces the truncation behavior many beginners expect. This single flag accounts for a large share of "my protein is too long" debugging sessions.

**Reverse complement and complement are interchangeable.** They differ by a reversal. A bare complement read left to right is not a valid strand direction, and downstream tools that expect 5 prime to 3 prime input will misinterpret it.

**The Alphabet argument makes sequences safer.** The opposite is true today. The Alphabet system is gone from current Biopython, and explicit codon tables passed to `translate()` are the modern way to declare molecular context.

## Common Mistakes and Limitations

- **Translating the template strand.** If your sequence came from the strand opposite the coding strand, `transcribe()` and `translate()` operate on the wrong molecule. Take `reverse_complement()` first when the source is the template strand.
- **Ignoring codon table selection.** The default translation uses the standard code. Bacterial and mitochondrial sequences need an explicit `table=` argument, and the difference is not cosmetic, it changes which codons terminate translation.
- **Assuming quality scores live on the Seq object.** In a SeqRecord from a FASTQ file, quality scores live in `record.letter_annotations["phred_quality"]`, not on `record.seq` itself. A Seq object has no per-base qualities.
- **Building sequences by repeated string concatenation.** Immutability means each concatenation copies the whole sequence. Accumulate fragments in a list and join once, or use MutableSeq.
- **Expecting Seq to scale to whole genomes in memory.** The Seq object holds one sequence in RAM. For random access across a large genome file, use the indexing functions in SeqIO or a dedicated indexed FASTA approach rather than loading everything.
- **Mixing string and Seq comparisons.** Comparing a Seq to a string directly has been version sensitive. Normalize with `str(seq) == my_string` or compare Seq to Seq.
- **Trusting ambiguous character handling silently.** N-rich regions translate to runs of X, and `to_stop=True` still stops at a real stop codon even inside poor-quality sequence. Check input composition before drawing biological conclusions from translated output.

## Quick Review

- A Seq object stores one biological sequence and behaves like a string while adding `.complement()`, `.reverse_complement()`, `.transcribe()`, `.back_transcribe()`, and `.translate()`.
- Seq is immutable. Transforms return new objects, and MutableSeq covers genuine in-place editing.
- `translate()` runs to the end by default. Use `to_stop=True` to truncate at the first stop codon, `cds=True` to validate a coding sequence, and `table=` for non-standard genetic codes.
- GC content comes from `gc_fraction()` in `Bio.SeqUtils` in current releases, returning a fraction you multiply by 100 for percent.
- SeqRecord wraps a Seq with id, description, annotations, and per-base quality data, and `Bio.SeqIO` is the standard reader and writer for FASTA, FASTQ, GenBank, and related formats.
- The Alphabet system from older tutorials is removed in current Biopython. Pass codon tables explicitly instead.
- Test with round trip invariants, such as reverse complementing twice to recover the original sequence.

## Installing and Verifying Your Biopython Environment

Most frustration with Seq objects traces back to environment problems rather than logic problems, so it is worth standardizing your setup before writing any sequence code. Create an isolated environment so that an upgrade elsewhere cannot silently change how your scripts behave:

```bash
python -m venv bioenv
source bioenv/bin/activate
pip install biopython
```

Then verify what you actually have, because the correct form of every example in this article depends on the installed version:

```python
import Bio
print(Bio.__version__)
```

If you work across several machines or a shared cluster, record this number in the header comment of every analysis script. Sequence code is unusually sensitive to library history: the GC function was renamed, the Alphabet system was removed, and comparison semantics shifted across releases, so two scripts that look identical can behave differently on two installations. A one-line version print is the cheapest debugging tool in this field.

A second verification step catches partial or corrupted installs. Run the following and confirm that nothing raises:

```python
from Bio.Seq import Seq
from Bio.SeqUtils import gc_fraction
from Bio import SeqIO

s = Seq("ACGT")
assert s.reverse_complement() == Seq("ACGT")
assert abs(gc_fraction(s) - 0.5) < 1e-9
print("Biopython", Bio.__version__, "OK")
```

Note the import style. Importing `Seq` from `Bio.Seq` rather than importing the whole package keeps startup fast and makes failures legible: if you see an `ImportError` naming `gc_fraction`, you know immediately that your installed Biopython predates the rename and you can either upgrade or fall back to the older function name deliberately, with a comment recording why.

Finally, keep test data out of your working directory. Create a `data/` folder for input FASTA and FASTQ files and an `out/` folder for results, and have scripts create `out/` on demand. Every worked example below assumes this layout.

## What Each Seq Method Does and Does Not Do

![Decision path asking whether the sequence is the coding strand, then whether cds=True validates, ending in translate or reverse complement first.](/article-images/illustrations/biopython-seq-object-tutorial-strand-orientation.webp)

The Seq object's methods are best understood as character transformations with biological conventions, not as validators of molecular identity. Nothing in a Seq object marks it as DNA, RNA, or protein, which has an important consequence: every method will happily run on any characters you give it, and it is your job to feed the right molecule in.

`transcribe()` is a pure T to U swap. If you apply it to a sequence that is already RNA, it does nothing harmful, it simply finds no T to replace. `back_transcribe()` is the mirror image. Neither method knows or checks whether you are holding a coding strand or a template strand, which is why the earlier material in this guide emphasizes taking a `reverse_complement()` first when your source is the template strand. If you are ever unsure which strand you have, the practical check is biological rather than programmatic: a true coding strand, when translated with `to_stop=True` and `cds=True`, should begin with a start codon and pass validation. Feed both orientations through `translate(cds=True)` and see which one validates.

`complement()` and `reverse_complement()` map characters through base pairing rules, including IUPAC ambiguity codes, and leave any character they do not recognize unchanged. This is convenient but also a trap: a sequence containing a stray whitespace character or a hyphen gap will pass through complementation with that character intact, and the error surfaces later, often as a translation failure or a garbage result. Normalize input early:

```python
clean = Seq(str(raw_seq).upper().replace(" ", "").replace("-", ""))
```

Calling `.upper()` at ingestion is a habit worth building. FASTA files from different sources arrive in mixed case, and while Biopython's own methods are generally tolerant, downstream tools you pipe output into are not uniformly so. Normalizing once at the boundary of your pipeline means every later step can assume one canonical representation.

`translate()` deserves the same explicitness. It does not inspect whether its input is DNA or RNA; U codons translate fine. It also does not demand a multiple of three. A trailing partial codon is handled gracefully, but a sequence whose length is not divisible by three is usually a sign that your coordinates are wrong, so treat `len(seq) % 3 != 0` on a supposed coding sequence as a bug report from your own pipeline, not a condition to work around.

## Finding Open Reading Frames: A Complete Walkthrough

Open reading frame detection is the canonical intermediate exercise for the Seq object, and it exercises slicing, translation, and strand handling in one place. The strategy: scan every offset in the sequence for a start codon, translate from that position to the first stop codon, and keep candidates long enough to be interesting.

```python
from Bio.Seq import Seq

def find_orfs(nuc_seq, min_aa=50):
    """Scan both strands for ORFs of at least min_aa amino acids."""
    results = []
    for strand, s in [("+", nuc_seq), ("-", nuc_seq.reverse_complement())]:
        for i in range(len(s) - 2):
            if s[i:i+3] == "ATG":
                protein = s[i:].translate(to_stop=True)
                orf_end = i + (len(protein) + 1) * 3
                ended_at_stop = orf_end <= len(s)
                if len(protein) >= min_aa:
                    results.append({
                        "strand": strand,
                        "start": i + 1,        # 1-based coordinate
                        "nt": len(protein) * 3 + 3,
                        "ended_at_stop": ended_at_stop,
                        "protein": protein,
                    })
    return results

contig = Seq("GGGTTACGATCTAGCATGAAAGCTTTTGGACCCTAACGTATGCC")
for orf in find_orfs(contig, min_aa=5):
    print(orf["strand"], orf["start"], orf["protein"], orf["ended_at_stop"])
```

Output:

```
+ 16 MKAFGP True
```

Walk through what happened. The function visits both strands, using the reverse complement to represent the minus strand read 5 prime to 3 prime. For every position where the next three characters are ATG, it slices from that position to the end and translates with `to_stop=True`, which returns the peptide up to but not including the stop codon. The `ended_at_stop` flag answers a question beginners rarely think to ask: did this ORF terminate at a real stop codon, or did the translation simply run off the end of the contig? The arithmetic is direct. The peptide has `len(protein)` residues, the stop codon adds one more codon, so the ORF occupies `i` through `i + (len(protein) + 1) * 3`, and if that exceeds the sequence length, the ORF is truncated at the contig boundary.

Three subtleties are worth internalizing from this example. First, nested ATGs: any ATG inside a longer ORF also produces a candidate, so the same region can appear multiple times with increasing lengths. This is usually desirable, since you cannot know which start codon a cell used, but it means downstream deduplication by stop codon position is often the right next step. Second, the scan is frame independent because it checks every position rather than stepping in threes from a fixed origin. That is slower but complete, and for contigs of realistic analysis size the cost is trivial. Third, the returned coordinates are 1-based on the strand where the ORF was found, which is the convention biologists expect in reports, but remember that minus strand coordinates refer to the reverse complement, not to the original file's coordinate system. If you report genomic coordinates later, you must map them back explicitly.

One more practical note: this function treats only ATG as a start. Under alternative genetic codes and in specific organisms, other codons initiate translation. If your organism requires a nonstandard code, pass the appropriate `table=` argument inside the translate call, and the same skeleton works unchanged.

## A Worked Primer Design Example

Primer design from a template sequence is a compact demonstration of slicing, reverse complementation, and composition arithmetic working together. The workflow: take the first stretch of the template as the forward primer, take the reverse complement of the final stretch as the reverse primer, and check composition on both.

```python
from Bio.Seq import Seq
from Bio.SeqUtils import gc_fraction

template = Seq("GGGTTACGATCTAGCATGAAAGCTTTTGGACCCTAACGTATGCC")

PRIMER_LEN = 20

fwd = template[:PRIMER_LEN]
rev = template[-PRIMER_LEN:].reverse_complement()

def wallace_tm(primer):
    s = str(primer).upper()
    at = s.count("A") + s.count("T")
    gc = s.count("G") + s.count("C")
    return 2 * at + 4 * gc

for name, primer in [("forward", fwd), ("reverse", rev)]:
    print(name, primer,
          f"GC {gc_fraction(primer) * 100:.1f}%",
          f"Tm(wallace) {wallace_tm(primer)}C")
```

Output:

```
forward GGGTTACGATCTAGCATGAA GC 45.0% Tm(wallace) 58C
reverse GGCATACGTTAGGGTCCAAA GC 50.0% Tm(wallace) 60C
```

Read the details carefully, because each line encodes a convention. The reverse primer is built from the reverse complement of the template's tail, not the tail itself, because the reverse primer anneals to the opposite strand. Getting this backwards is the single most common error in beginner primer scripts, and the symptom is subtle: the primer sequence looks plausible and even has sensible composition, it simply anneals to nothing. The `[-PRIMER_LEN:]` slice takes the last twenty characters, and slicing from a negative index is half-open in the same way as forward slicing, so there is no off-by-one at the end of the sequence.

The Wallace rule used here is a teaching heuristic that estimates melting temperature from base counts alone. It is deliberately simple so the example stays focused on Seq mechanics. For real assay work, Biopython provides nearest-neighbor thermodynamic calculations in `Bio.SeqUtils.MeltingTemp`, which account for effects the simple count ignores. The structural point of the example stands regardless of which Tm method you adopt: because Seq objects behave like strings under composition counting but know [base pairing](/knowledge/molecular-biology/base-pairing) under complementation, a primer script reads as a short list of biological statements rather than a pile of character manipulation.

A final design note for the script skeleton above. The primer length lives in one named constant at the top. Resist the urge to hardcode lengths inside the slicing expressions, because primer design is a parameter exploration, and you will rerun this with different values, possibly across many candidate regions in a loop.

## Ambiguity Codes in Depth

Ambiguous IUPAC codes are where naive string handling and Seq object handling diverge most sharply. The full single letter set you will encounter in nucleotide data is:

- A, C, G, T: unambiguous bases.
- R (A or G) and Y (C or T): purine and pyrimidine.
- S (G or C) and W (A or T): strong and weak pairing.
- K (G or T) and M (A or C): keto and amino.
- B (C, G, or T), D (A, G, or T), H (A, C, or T), V (A, C, or G): three-way choices, each the complement of one of the two-way codes.
- N: any base, the code you will see most often in draft assemblies.

Two properties follow. Complementation preserves ambiguity class correctly, so the reverse complement of an R is a Y, and the reverse complement of a B (not an A) is a V (not a T). This is exactly the behavior that makes round trip tests meaningful on ambiguous input, as in the test `Seq("ACGTNRYSW").reverse_complement().reverse_complement() == Seq("ACGTNRYSW")` shown earlier in this guide. A hand-rolled translation table keyed on four bases cannot reproduce it.

Translation resolves ambiguity as far as the genetic code permits, and this is worth seeing concretely, because the results are often surprising in a useful way:

```python
from Bio.Seq import Seq

print(Seq("GCN").translate())  # Seq('A'), all 16 GCN codons are alanine
print(Seq("ATH").translate())  # Seq('I'), ATA, ATC, ATT are all isoleucine
print(Seq("AAR").translate())  # Seq('K'), AAA and AAG are both lysine
print(Seq("NNN").translate())  # Seq('X'), cannot be resolved
```

The first three results follow from degeneracy: some codon families are unaffected by variation at the third position, so a fully ambiguous third base still determines a single residue. Only when ambiguity genuinely blocks resolution does X appear. This graded behavior, rather than an all-or-nothing failure, is what makes it practical to translate draft assemblies containing N runs and still recover most of the [protein sequence](/blog/guides/protein-sequence).

Practical hygiene follows from these mechanics. Count ambiguity before trusting any derived quantity:

```python
def ambiguous_fraction(seq):
    s = str(seq).upper()
    unambiguous = sum(s.count(b) for b in "ACGT")
    return 1 - unambiguous / len(s) if len(s) else 1.0
```

A contig with a high ambiguous fraction will translate into long X runs and inflated apparent protein lengths, so screening on this number before translation saves downstream confusion. Also watch for gap characters, hyphens and dots, which appear when sequences have passed through alignment software. They are not ambiguity codes, they are padding, and they should be stripped before translation rather than carried through.

## Extracting Coding Sequences from GenBank Features

When a record arrives from a database rather than from your own assembly, the biology is already annotated, and the Seq object connects to that annotation through SeqRecord features. This is the step where many self-taught scripts go wrong by re-deriving coordinates by hand, so the recommended approach is to let feature objects do the extraction:

```python
from Bio import SeqIO

record = SeqIO.read("record.gb", "genbank")

for feature in record.features:
    if feature.type != "CDS":
        continue
    cds = feature.extract(record.seq)
    protein = feature.qualifiers.get("translation", [None])
    locus_tag = feature.qualifiers.get("locus_tag", ["?"])
    strand = feature.location.strand
    print(locus_tag, strand, len(cds), len(cds) % 3)
```

The key call is `feature.extract(record.seq)`. It slices the record's sequence according to the feature's stored location, and it does so correctly for the cases that break manual slicing: features on the minus strand are automatically reverse complemented, and compound locations built from multiple joined segments, which are the norm in eukaryotic and many bacterial annotations, are assembled in the right order with introns excluded. Reproducing that logic by hand from raw location strings is exactly the kind of code that works on your first test record and fails on the hundredth.

The `qualifiers` dictionary is the annotation payload. Values are always lists, even for singletons, which is why the `.get(..., [None])` pattern appears above: it provides a default list and then takes its single element. The printed `len(cds) % 3` is a cheap audit. A CDS whose length is not a multiple of three is either a pseudogene, a frameshift, an annotation error, or a partial feature at a contig edge, and knowing which category a record falls into matters before you compare its protein to anything. Related tooling in the wider ecosystem follows the same spirit of extracting features into usable files, for example utilities that pull features from GFF3 and GTF annotations out into FASTA and BED formats [12], and the principle is identical regardless of the annotation format: extraction should be driven by the annotation's own coordinates, never by hardcoded offsets.

A common follow-up question is how to get the [nucleotide sequence](/knowledge/molecular-biology/nucleotide-sequence) of a gene with upstream flanking region for promoter analysis. The recipe is a manual slice plus an explicit strand check:

```python
for feature in record.features:
    if feature.type == "CDS":
        start = int(feature.location.start)
        end = int(feature.location.end)
        upstream = record.seq[max(0, start - 200):start]
        if feature.location.strand == -1:
            upstream = upstream.reverse_complement()
```

The `max(0, ...)` guard handles features near the start of the record, where a naive negative slice would silently wrap around and grab the end of the sequence, a classic Python slicing surprise with biological consequences.

## Working with FASTQ Records and Quality Data

FASTQ is where the boundary between Seq and SeqRecord stops being academic. A Seq object holds bases and nothing else, but a sequencing read is useless without its per-base quality scores, and those scores live on the SeqRecord, not on the Seq:

```python
from Bio import SeqIO

for record in SeqIO.parse("reads.fastq", "fastq"):
    quals = record.letter_annotations["phred_quality"]
    mean_q = sum(quals) / len(quals)
    if mean_q >= 25 and len(record.seq) >= 50:
        pass  # keep the record for assembly or mapping
```

Two conventions matter here. First, `record.letter_annotations` is keyed per letter, meaning per base, and `phred_quality` is the standard key Biopython populates when parsing FASTQ. Second, the quality threshold in the comparison above is written as a named parameter of your analysis, not a universal constant; appropriate values depend on your platform and library preparation, and copying someone else's threshold without checking it against your data is a recurring source of quietly biased read sets.

Quality filtering of next generation sequencing reads is such a well established workflow step that entire tool suites for manipulating the known FASTQ format variants were implemented in Python for the Galaxy platform [4]. Biopython's SeqIO covers those format variants at parse time, so an Illumina style file and a Sanger style file both produce the same SeqRecord shape in memory, with the quality encoding differences resolved during parsing rather than left to your code.

One operational warning: because FASTQ records carry two parallel streams, bases and qualities, any operation that reorders or subsets reads must move both together. Dropping reads is safe, since a SeqRecord travels as one unit, but if you ever rebuild records by hand from a base list and a quality list, a length mismatch will produce a file that parses without complaint and pairs the wrong quality value to each base. Defend against this with a one line assertion that `len(record.seq) == len(quals)` before writing anything.

## Coordinates, Slicing, and the Off-by-One Traps

Biological coordinates and Python coordinates disagree, and the disagreement generates a steady drip of subtle errors. The rules:

- Python slices are half-open: `seq[4:10]` returns characters at indices 4 through 9, ten minus four equals six characters.
- GenBank style biological locations are 1-based and inclusive: a location written 5..10 covers six bases.
- The conversion is mechanical: a biological range from `b_start` to `b_end` corresponds to `seq[b_start - 1 : b_end]`.

Verify the conversion on a concrete example so your fingers learn it:

```python
from Bio.Seq import Seq

s = Seq("ACGTTGCAAGGTCCAA")
# biological positions 5..10 are the bases T T G C A A
assert s[4:10] == Seq("TTGCAA")
```

Three follow-on traps compound this. First, negative indices count from the end, which is convenient for taking the tail of a sequence for a reverse primer, but catastrophic if a variable that should be positive is accidentally negative, since the slice will not fail, it will return the wrong end of the molecule. Second, a slice beyond the end of the sequence does not raise an error, it simply returns whatever exists. `seq[1000:2000]` on an 800 base sequence returns an empty Seq, not an exception, so an extraction routine with a coordinate bug can emit empty outputs that propagate downstream. Add explicit length checks when coordinates come from external files. Third, strand matters at the coordinate level. When a feature sits on the minus strand, its biological start is greater than its biological end in GenBank's complement notation, and Biopython's location objects normalize this for you through `feature.location.strand`, which is one more reason to prefer `feature.extract()` over manual slicing whenever an annotation exists.

When you report results, state the convention next to the numbers. A line like `orf_start=16 (1-based, plus strand)` costs nothing to print and prevents the most tedious class of follow-up question from collaborators.

## Motif Searching on Both Strands

Counting and locating a short pattern is a daily task, and Seq objects support it through string-like behavior plus complementation. The loop below finds every occurrence of a motif, including overlapping ones, which matters because biological sites frequently overlap:

```python
def find_all(seq, motif):
    hits = []
    start = 0
    while True:
        idx = str(seq).find(motif, start)
        if idx == -1:
            break
        hits.append(idx)
        start = idx + 1   # advance by one so overlaps are found
    return hits
```

Note the advance by one rather than by the motif length. Search for GATC in a sequence containing GATCATC and you will find both occurrences, which a non-overlapping count misses. This is the same distinction the `count` method encodes in its `overlap` argument.

For double stranded DNA, a motif on the minus strand appears in the file as its reverse complement, so a complete search covers both orientations:

```python
def find_both_strands(seq, motif):
    motif_rc = str(Seq(motif).reverse_complement())
    return find_all(seq, motif), find_all(seq, motif_rc)
```

An instructive special case is the palindromic motif, such as the GAATTC restriction site. A palindrome equals its own reverse complement, so both searches return identical hit lists, and noticing this in your own data is a good sanity check that your reverse complement logic is correct. For non-palindromic motifs the two lists differ, and their union is the complete answer. If your motifs contain ambiguity codes, plain `find` will not match them, and the general solution is to expand the motif into a regular expression using the IUPAC expansion, for example converting R into the character class of A and G, then search with the standard library's regular expression module. Keep the expansion table in one place in your codebase, because it doubles as documentation of which codes your pipeline supports.

## Performance: Choosing Between str, Seq, MutableSeq, and Indexed Files

Sequence code rarely fails on speed at small scale, so performance problems tend to surface late, on real data, in the middle of a long analysis. Four choices determine most of the outcome.

First, transformation-heavy code on single sequences is essentially free. Every Seq method is a single pass over the characters, and calling several in a chain creates several small intermediates, which is irrelevant at contig scale. Do not pre-optimize this.

Second, accumulation is the real hazard. Because Seq is immutable, `seq += fragment` inside a loop copies the entire growing sequence on every iteration, so the total cost grows with the square of the final length in the worst case. For reads assembled from fragments, the correct patterns are a list joined once, or MutableSeq for genuinely in-place construction:

```python
from Bio.Seq import MutableSeq

chunks = []
for piece in source_of_pieces():
    chunks.append(piece)
assembled = Seq("".join(chunks))
```

Third, whole-genome scale is a memory question, not a speed question. A Seq object holds its entire sequence in RAM, so loading every chromosome of a large genome into a list is a design decision with real consequences. When you need random access to named records from a large file, index it instead of loading it:

```python
from Bio import SeqIO

index = SeqIO.index("genome.fasta", "fasta")
record = index["contig_0042"]      # parses only this record
print(len(record.seq))
```

An index stores file offsets and parses a record only when requested, so memory stays flat regardless of file size. For collections of many files, the database-backed variant of the same interface exists in SeqIO. This layering, small Seq objects in memory for computation and indexed file access for storage, mirrors how larger systems in the ecosystem organize their work: Plastid, for example, represents sequencing data as arrays of values tied to genomic or transcriptomic positions precisely so that nucleotide resolution analyses do not require whole molecules in memory at once [6].

Fourth, be honest about where time actually goes. Before restructuring working code, time it on a realistic input. Most sequence pipelines spend their time in external tools and file parsing, not in Seq method calls, and replacing a clear loop with a clever one usually costs more in correctness risk than it returns in seconds.

## Troubleshooting Guide: Error Messages and Their Fixes

These are the failures you are most likely to meet, in roughly the order they appear in a beginner's first week.

**`AttributeError: 'str' object has no attribute 'reverse_complement'`.** You called a biological method on a plain string, usually one obtained by slicing `record.description` instead of `record.seq`, or from a split header line. Wrap it: `Seq(s).reverse_complement()`. Better, find where the string came from and keep the Seq object in play.

**A `TypeError` from the Seq constructor when passing two arguments.** This is the signature of copied tutorial code using the removed Alphabet system. Drop the second argument. If you need to declare molecular context, do it at the operation that cares, for example with `table=` at translation.

**`ImportError: cannot import name 'gc_fraction'`.** Your installed Biopython predates the rename. Either upgrade the package or use the older function that returned a percentage directly, but choose deliberately and note the choice in the script, because the two return different units and code that mixes them produces GC values that are silently off by a factor of one hundred.

**`ValueError` from `SeqIO.read` complaining about multiple records.** `SeqIO.read` is for single record files. For anything multi-record, use `SeqIO.parse` and iterate, or take the first record explicitly with `next(SeqIO.parse(...))` when you truly want only the first, and say so in a comment.

**A translation error mentioning an invalid character.** Your sequence contains a character outside the codon table, almost always a gap or whitespace that survived ingestion. Strip gaps and whitespace at the boundary, as shown in the normalization snippet earlier, and retranslate.

**Protein output that is absurdly long.** You forgot `to_stop=True`, so every in-frame stop codon is an asterisk in the middle of the peptide. Grep the translation for asterisks before using it.

**A reverse primer that will not anneal.** You took the reverse complement of the wrong end, or complemented without reversing. Run your round trip test: reverse complementing your candidate twice must return the template tail exactly.

**Results differ between two machines.** Print `Bio.__version__` on both. Version skew explains most cross-machine disagreement in this ecosystem, and it is a two-minute check that frequently ends an hour of debugging.

## Migrating Old Biopython Code: A Practical Checklist

A large fraction of code found in older course materials, forum posts, and published supplementary scripts predates the current API. Use this checklist when reviving any of it.

- **Remove every Alphabet import and argument.** Constructor calls like `Seq("AGTACACTGGT", some_alphabet)` must lose the second argument. Any code branching on `seq.alphabet` must be replaced with explicit logic, typically a codon table passed where it is used.
- **Rename GC to gc_fraction.** The old function returned a percentage, the new one a fraction, so after renaming, multiply by one hundred wherever a percentage is displayed. Check every downstream comparison, because a filter written for percentages will reject everything if fed fractions.
- **Re-examine Seq to string comparisons.** Comparison behavior between the two types has varied across releases. Normalize both sides with `str()` before comparing, or compare Seq to Seq, and add a small assertion that documents the expectation.
- **Replace declared alphabets at translation sites with explicit codon tables.** `table=11` for typical bacterial coding sequences and `table=2` for vertebrate mitochondrial sequences are the cases that appear most often in migrated pipelines.
- **Audit every `.count("GG")` style call for overlap semantics.** Older scripts frequently intended overlapping matches; make the intent explicit with the `overlap` argument where it matters.
- **Run your known-good fixture.** Keep a short input sequence with its known reverse complement, transcription, and translation in the test file, as recommended earlier in this guide. Passing the fixture after migration is the fastest available evidence that the port preserved semantics.

The general principle behind the checklist is that the modern API pushes molecular context from declaration time to operation time. Old code told the object what it was; new code tells each operation what it needs. Migrating means moving those statements to where they are actually consumed, which usually makes the code easier to read as a bonus.

## From GenBank Annotation to a Protein FASTA: A Second Pipeline

Building on the feature extraction section, here is a compact end-to-end script that reads a GenBank file, extracts every annotated CDS, translates it under a chosen genetic code, and writes a protein FASTA with informative identifiers. It is a realistic skeleton for the preparation steps before comparative or structural analyses.

```python
from Bio import SeqIO
from Bio.SeqRecord import SeqRecord

CODE_TABLE = 11          # adjust per organism before running
records_out = []

for record in SeqIO.parse("genome.gb", "genbank"):
    for feature in record.features:
        if feature.type != "CDS":
            continue
        cds = feature.extract(record.seq)
        if len(cds) % 3 != 0 or len(cds) == 0:
            print("skipping", feature.qualifiers.get("locus_tag", ["?"]),
                  "len", len(cds))
            continue
        protein = cds.translate(table=CODE_TABLE, to_stop=True, cds=False)
        tag = feature.qualifiers.get("locus_tag", ["unnamed"])
        out = SeqRecord(
            protein,
            id=f"{record.id}_{tag}",
            description=f"len={len(protein)} table={CODE_TABLE}",
        )
        records_out.append(out)

SeqIO.write(records_out, "proteins.fasta", "fasta")
print(f"wrote {len(records_out)} proteins")
```

Design decisions in this script reward a second look. The multiple-of-three guard runs before translation, so malformed features are reported by locus tag rather than producing mysterious downstream failures. Translation uses `to_stop=True` so that internal stop annotations, which occur in pseudogenes and frameshifted features, truncate the peptide rather than threading asterisks through the output file. The output identifiers concatenate the source record identifier with the locus tag, which keeps the protein file self-documenting when it is later joined against the nucleotide file. And the description field records the codon table used, because in mixed collections of genomes, bacterial and mitochondrial style codes can coexist, and a protein file without provenance is a file you will distrust in six months.

If your next step is phylogenetic, the protein FASTA feeds alignment and tree building, and dedicated libraries such as ETE handle the manipulation, analysis, and visualization of the resulting phylogenetic and clustering trees from Python [5]. If it is population genetic, EggLib provides the coalescent simulation and neutrality testing machinery behind a Python interface [2]. In both cases the file you just produced is the handoff point, which is why getting the identifiers and provenance right here pays forward.

## Practical Applications Across the Ecosystem

Seeing where Seq objects sit inside real published tools helps calibrate how much rigor each stage deserves. Several patterns recur.

In assembly scale genomics, PHYLUCE takes enriched sequencing data through contig assembly, targeted locus identification, and alignment preparation across large taxon samples [9], and every stage of that chain consumes and produces plain sequence data of exactly the kind a Seq object represents. The lesson for your own scripts is that intermediate files between stages should be boring, standard FASTA, with identifiers stable across stages, so that any stage can be rerun or replaced independently.

In population genetics, EggLib combines a fast C++ core with a high level Python interface for analyses such as coalescent simulation and neutrality tests [2]. Its Python layer is the same design philosophy as Seq: expose biological meaning through the interface, hide the fast implementation below. When you write helper functions over Seq objects, aim for that same shape, a small vocabulary of named operations rather than a grab bag of utilities.

In annotation mining, tools such as GAD convert GFF3 and GTF annotations into formats like BED and FASTA [12], and ProdMX analyzes protein domains across millions of genomes using compressed sparse representations [13]. Both begin with sequences extracted on annotation coordinates, which is the workflow detailed in the GenBank features section above, scaled up. Similarly, PyGMQL demonstrates how region based queries scale across thousands of heterogeneous experiments on distributed infrastructure [11], a reminder that the coordinate conventions you practice on single records are the same ones that must hold at cohort scale.

In structural bioinformatics, Biopython itself hosts the interface to the SCOP and ASTRAL structural classification resources, written to work as part of the Biopython distribution [3], so protein sequences you produce with `translate()` flow naturally toward structural classification workflows without format friction.

In clinical variant work, the hgvs package parses, validates, maps, and formats sequence variant descriptions under Human Genome Variation Society nomenclature [7], and VariantValidator builds a web service on it that corrects common nomenclature mistakes and interconverts HGVS and Variant Call Format descriptions [8]. Reference sequence data in such pipelines is exactly the material Seq objects hold, and the same discipline applies: reference bases are immutable inputs, every derived representation is a new object, and provenance is recorded alongside identifiers.

Even the earliest experimental modules in this space, such as the Sarment segmentation package [1], point at the same conclusion this guide keeps returning to: the field converged on Python for sequence work because a readable, string-like sequence object with biological methods is the right abstraction, and Biopython's Seq is the reference implementation of that idea.

## A Testing Checklist for Sequence Code

Because the earlier material introduced round trip tests, it is worth closing with a fuller checklist you can apply to any script built on Seq objects. Each item is a property that should hold regardless of input.

```python
import pytest
from Bio.Seq import Seq

@pytest.mark.parametrize("seq_str", ["ACGT", "ACGTNRYSW", "TTAGGGTCCAAA"])
def test_reverse_complement_is_involutive(seq_str):
    s = Seq(seq_str)
    assert s.reverse_complement().reverse_complement() == s

@pytest.mark.parametrize("seq_str", ["ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG"])
def test_transcription_round_trip(seq_str):
    s = Seq(seq_str)
    assert s.transcribe().back_transcribe() == s

def test_known_translation():
    s = Seq("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG")
    assert s.translate(to_stop=True) == Seq("MAIVMGR")

@pytest.mark.parametrize("bad", ["ATGGCT", "ATG"])  # lengths not multiples of 3
def test_partial_codon_flagged(bad):
    assert len(bad) % 3 != 0  # your pipeline should treat this as a warning site
```

Interpret the checklist as questions to ask of any new sequence code, whether or not you write formal tests. Does reverse complementing twice restore the input, including ambiguous characters? Does transcribing and back transcribing restore the input? Does the known-good fixture still translate to its known peptide? Are supposed coding sequences flagged when their length is not a multiple of three? Does any code path slice beyond the end of a sequence and silently receive an empty result? Is the codon table stated explicitly wherever translation occurs? Do FASTQ outputs carry qualities that match their bases in length and order?

None of these checks requires domain specific data, all of them run in milliseconds, and together they catch the failure modes that sequence code is uniquely prone to: transformations that succeed numerically while being biologically wrong. In a field where the wrong answer usually looks like a perfectly plausible string, that is the difference between a pipeline you trust and one you debug after publication.

## Frequently Asked Questions

### What is a Biopython Seq object?

It is a class from Biopython's `Bio.Seq` module that stores a DNA, RNA, or protein sequence and provides biological methods such as reverse complementation and translation. It is immutable and behaves like a Python string for slicing, indexing, and iteration.

### How is a Seq object different from a plain Python string?

A Seq object answers sequence questions a string cannot, such as the reverse complement or the translation of a coding sequence under a chosen genetic code. A plain string treats every character as text and offers no biological operations.

### How do I compute GC content from a Seq object?

Call `gc_fraction()` from `Bio.SeqUtils`, which returns a fraction between 0 and 1, and multiply by 100 for a percentage. Older Biopython code uses a different function name in the same module, so check your installed version before copying imports.

### How do I translate a DNA sequence into protein?

Call `translate()` on the Seq object. Add `to_stop=True` to end at the first in-frame stop codon, `cds=True` to validate a complete coding sequence, and `table=11` or `table=2` for bacterial or vertebrate mitochondrial codes respectively.

### When should I use SeqRecord instead of Seq?

Use Seq whenever you need the sequence alone. Use SeqRecord whenever the sequence carries an identifier, description, annotations, or quality scores, which is almost always the case for data read from or written to files through SeqIO.

### Do I still need to pass an Alphabet to the Seq constructor?

No. The Alphabet system was deprecated and removed from Biopython, so constructor calls with alphabet arguments from older tutorials will not run on current versions. Declare molecular context explicitly, for example with a codon table argument at translation time.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is a Biopython Seq object?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "It is a class from Biopython's Bio.Seq module that stores a DNA, RNA, or protein sequence and provides biological methods such as reverse complementation and translation. It is immutable and behaves like a Python string for slicing, indexing, and iteration."
      }
    },
    {
      "@type": "Question",
      "name": "How is a Seq object different from a plain Python string?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "A Seq object answers sequence questions a string cannot, such as the reverse complement or the translation of a coding sequence under a chosen genetic code. A plain string treats every character as text and offers no biological operations."
      }
    },
    {
      "@type": "Question",
      "name": "How do I compute GC content from a Seq object?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Call gc_fraction() from Bio.SeqUtils, which returns a fraction between 0 and 1, and multiply by 100 for a percentage. Older Biopython code uses a different function name in the same module, so check your installed version before copying imports."
      }
    },
    {
      "@type": "Question",
      "name": "How do I translate a DNA sequence into protein?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Call translate() on the Seq object. Add to_stop=True to end at the first in-frame stop codon, cds=True to validate a complete coding sequence, and table=11 or table=2 for bacterial or vertebrate mitochondrial codes respectively."
      }
    },
    {
      "@type": "Question",
      "name": "When should I use SeqRecord instead of Seq?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use Seq whenever you need the sequence alone. Use SeqRecord whenever the sequence carries an identifier, description, annotations, or quality scores, which is almost always the case for data read from or written to files through SeqIO."
      }
    },
    {
      "@type": "Question",
      "name": "Do I still need to pass an Alphabet to the Seq constructor?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No. The Alphabet system was deprecated and removed from Biopython, so constructor calls with alphabet arguments from older tutorials will not run on current versions. Declare molecular context explicitly, for example with a codon table argument at translation time."
      }
    }
  ]
}
</script>

## Sources

1. [Sarment: Python modules for HMM analysis and partitioning of sequences.](https://pubmed.ncbi.nlm.nih.gov/15947017/)
2. [EggLib: processing, analysis and simulation tools for population genetics and genomics.](https://pubmed.ncbi.nlm.nih.gov/22494792/)
3. [A high level interface to SCOP and ASTRAL implemented in python.](https://pubmed.ncbi.nlm.nih.gov/16403221/)
4. [Manipulation of FASTQ data with Galaxy.](https://pubmed.ncbi.nlm.nih.gov/20562416/)
5. [ETE: a python Environment for Tree Exploration.](https://pubmed.ncbi.nlm.nih.gov/20070885/)
6. [Plastid: nucleotide-resolution analysis of next-generation sequencing and genomics data.](https://pubmed.ncbi.nlm.nih.gov/27875984/)
7. [A Python package for parsing, validating, mapping and formatting sequence variants using HGVS nomenclature.](https://pubmed.ncbi.nlm.nih.gov/25273102/)
8. [VariantValidator: Accurate validation, mapping, and formatting of sequence variation descriptions.](https://pubmed.ncbi.nlm.nih.gov/28967166/)
9. [PHYLUCE is a software package for the analysis of conserved genomic loci.](https://pubmed.ncbi.nlm.nih.gov/26530724/)
10. [metaseq: a Python package for integrative genome-wide analysis reveals relationships between chromatin insulators and associated nuclear mRNA.](https://pubmed.ncbi.nlm.nih.gov/25063299/)
11. [PyGMQL: scalable data extraction and analysis for heterogeneous genomic datasets.](https://pubmed.ncbi.nlm.nih.gov/31703553/)
12. [GAD: A Python Script for Dividing Genome Annotation Files into Feature-Based Files.](https://pubmed.ncbi.nlm.nih.gov/32524530/)
13. [ProdMX: Rapid query and analysis of protein functional domain based on compressed sparse matrices.](https://pubmed.ncbi.nlm.nih.gov/33335686/)