BIO-1-1

Represent DNA, RNA, and protein as strings

16 min

Here is the good news you already suspected: a genome really is just a string. The whole spine you just finished, the double helix from S3, transcription from S4, the codon table and proteins from S5 and S6, collapses into text you can slice, index, and grep. This is the point in the course where your existing skills convert fastest, and where a strong software engineer can suddenly move quicker than a career biologist at a keyboard.

Here is the trap in the same sentence. A biological sequence is a string wearing three pieces of hidden state that a plain String type does not model: a reading direction, a silent partner strand, and a coordinate convention that lives outside the string entirely. Almost every classic bioinformatics bug is one of those three leaking out. So we will take the comfort, then spend the rest of the lesson earning the right to keep it.

The alphabets, precisely

A sequence is a string over a small, fixed alphabet. There are three that matter, and their alphabets are the first thing you commit to memory.

  • DNA is a string over the four bases A, C, G, T (adenine, cytosine, guanine, thymine).
  • RNA is the same alphabet with one substitution: T becomes U (uracil), giving A, C, G, U. Recall from S4 that transcription copies a DNA strand base for base and swaps T for U, so transcribe("ATGC") returns AUGC. Same information, one letter renamed.
  • Protein is a string over 20 letters, one per amino acid (recall the 20 residues from S5), plus a symbol for stop. In this course and in the frozen lib the stop is written as *, so a translated protein like MAST* means "start, then three more residues, then stop."

Two refinements you need before you write any code against these. First, real sequence files carry a fifth DNA character, N, which we get to next, so the practical DNA alphabet is A, C, G, T, N. Second, case carries meaning. Genome files often lowercase regions that are repetitive or low-complexity, a convention called soft-masking, so acgt and ACGT are the same bases flagged differently. The frozen transcribe and complementDNA preserve case exactly so a viewer can show those masked regions without losing them. Do not blindly uppercase a sequence and throw that signal away.

Ambiguity codes: N is a base, not a blank

When a sequencer is not confident which base sits at a position, it does not leave a hole. It writes N, the ambiguity code meaning "any of A, C, G, or T." N is a real character in the string, it occupies one position, and it participates in operations. By the pairing rule from S3.2 there is no single complement of "any base," so by convention N is its own complement, which keeps string operations total (every input maps to some output). That is exactly what the frozen complementDNA does: N maps to N.

There is a fuller IUPAC set (R for A or G, Y for C or T, and so on) for finer-grained uncertainty, but N is the one you will meet constantly.

Direction is not decoration

A plain string has no orientation. A DNA strand does. Recall from S3.2 that a strand runs from its 5 prime end to its 3 prime end, and that every enzyme that reads or builds DNA can only travel one way along it, from 5 prime toward 3 prime. By universal convention a sequence written on the page is read 5 prime to 3 prime unless stated otherwise. The direction is not in the characters, it is a promise about how to read them, and that promise is the first hidden field your String type is missing.

Direction is what makes the reverse complement more than trivia. The two strands of the helix are antiparallel: where one runs 5 prime to 3 prime, its partner runs the opposite way. So to write the partner strand in its own natural 5 prime to 3 prime direction you complement each base and then reverse the order. That is the reverseComplement from S3.2, and in the bioinformatics track you will call it constantly, because a sequencing machine may read either strand, so a large fraction of your reads arrive as the reverse complement of the reference you are trying to match.

Type a sequence into the tool below and watch three rows update live: the complement (pairing rule applied in place), the reverse complement (the partner strand as you would actually write it), and the transcribed mRNA. Before you edit, predict what happens to the reverse complement when you append one base to the right end. It changes at the left. If that surprises you, sit with it until it does not, because it is the whole reason the reverse comes after the complement.

transcribe.ts
coding DNA 5' to 3'
ATGGCACTGTAA
mRNA 5' to 3'
AUGGCACUGUAA
reverse complement 5' to 3'
TTACAGTGCCAT

Predict it yourself: what base pairs with the first base of the coding strand (A)?

Now the code, mirroring the frozen src/lib/bio/transcription.ts. Read it and confirm the reverse falls out of prepending each complemented base, so the loop reverses and complements in a single pass.

reverse_complement.ts
const COMPLEMENT: Record<string, string> = {
  A: "T", T: "A", C: "G", G: "C", N: "N",
}

function complementDNA(base: string): string {
  // Unknown characters return unchanged, so the function is total.
  return COMPLEMENT[base] ?? base
}

function reverseComplement(dna: string): string {
  let out = ""
  for (const ch of dna) {
    // Prepend each complemented base, which reverses the order in one pass.
    out = complementDNA(ch) + out
  }
  return out
}

// An alphabet check, case-insensitive, N allowed. Rejects U (that is RNA).
function isValidDNA(seq: string): boolean {
  return /^[ACGTN]+$/i.test(seq)
}

// reverseComplement("ATGC") is "GCAT"
// reverseComplement is an involution: rc(rc(x)) is x

The real lib also maps lowercase bases (to preserve soft-masking) and includes a separate RNA complement, but the shape is exactly this. Note isValidDNA rejects U on purpose: a U means someone handed you RNA, and running DNA logic on it is a bug you want to catch at the door, not three functions later.

Coordinates: the off-by-one that silently corrupts everything

This is the section that matters most, because it produces the most damage per line of code. When you name a region of a sequence, "the feature from position X to position Y," you are using a coordinate convention, and there is more than one in active daily use. Two independent choices, four combinations.

The first choice is where counting starts. 0-based numbers the first base 0 (like a Python or JavaScript array). 1-based numbers the first base 1 (like a human counting, or a text editor's line numbers). The second choice is whether the end is included. A closed interval [start, end] includes the base at end. A half-open interval [start, end) stops just before it, exactly like Python's s[start:end] slice.

The formats you will actually open split along these lines, and you must know which is which cold:

  • BED is 0-based, half-open. A feature covering the first ten bases is start = 0, end = 10, and its length is simply end - start.
  • GFF and GTF (gene annotations) are 1-based, closed. That same ten-base feature is start = 1, end = 10, and its length is end - start + 1.
  • VCF (variant calls) is 1-based, and its POS field points at the reference base 1-based, matching the GFF style. Same for SAM and BAM alignment positions.

So a BED "end" of 10 and a GFF "end" of 10 do not describe the same last base, and a length computed with the wrong formula is off by one every single time.

Read this panel, then convert by hand: what is VCF POS 5 in BED coordinates?

coordinates.py
# The SAME ten-base feature in two conventions.

# BED: 0-based, half-open  [start, end)
bed_start, bed_end = 0, 10
bed_len = bed_end - bed_start          # 10

# GFF / GTF / VCF: 1-based, closed  [start, end]
gff_start, gff_end = 1, 10
gff_len = gff_end - gff_start + 1      # 10

def one_based_closed_to_bed(start, end):
    # Shift the start left by one, keep the end. Length is preserved:
    # end - (start - 1) == end - start + 1
    return (start - 1, end)

# A single variant at VCF POS 5 (1-based) becomes, in BED:
#   start 4, end 5   (half-open, length 1)
print(one_based_closed_to_bed(5, 5))   # (4, 5)
The programmer analogy, and the exact edge where it breaks

"A genome is a byte buffer" is the analogy that gets you in the door, and it is genuinely good: sequences are immutable text, operations are string manipulation, and a chromosome file is a text file. Retire the analogy the moment you start reasoning about the three hidden fields. A byte buffer has no reading direction, but a strand can only be read 5 prime to 3 prime and its reverse complement is a different-looking string carrying identical information. A byte buffer stores one copy, but every DNA position implies a partner base on the other strand, so "the sequence" is really two synchronized strings and a bug can hit both. And a byte buffer's index means one unambiguous thing, but a biological coordinate is meaningless until you attach a convention (0 or 1 based, open or closed) that is not stored in the buffer at all. The analogy fails precisely where biology adds physics and history to the text. Treat a sequence as a string plus three annotations you carry by hand, and you keep the speed without the silent bugs.

Key terms

alphabet
The fixed set of characters a sequence is built from: A, C, G, T for DNA, A, C, G, U for RNA, and 20 letters plus a stop symbol for protein.
ambiguity code (N)
A character standing for an uncertain but present base. N means any of A, C, G, or T, occupies one position, and by convention is its own complement.
soft-masking
Writing repetitive or low-complexity regions in lowercase so they stay in the string but flagged. Case therefore carries meaning and must be preserved.
5 prime to 3 prime directionality
The fixed reading direction of a strand. Sequences are written and machinery reads them 5 prime to 3 prime, so a strand is a directed string, not an orientation-free one.
reverse complement
The partner strand written in its own 5 prime to 3 prime direction: complement each base, then reverse the order. Both strands carry the same information read opposite ways.
0-based vs 1-based
Whether the first base is numbered 0 (like an array, used by BED) or 1 (like human counting, used by GFF, GTF, VCF, SAM).
half-open vs closed interval
Whether the end position is excluded, [start, end), giving length end minus start (BED), or included, [start, end], giving length end minus start plus one (GFF, VCF).

Where this leaves you

A sequence is a string over a known alphabet, and that comfort is real. Keep it, but carry three annotations the string itself does not hold: which way it reads (5 prime to 3 prime, so reverse complement matters), that N is a present base rather than a blank, and which coordinate convention names its positions (0-based half-open for BED, 1-based closed for GFF and VCF). Get those right and the rest of the track is string manipulation you already know how to do. Get any one wrong and your pipeline runs green while lying to you. Next, in BIO-1.2, we turn these strings into proteins, where the reading frame makes the direction bite.

Check yourself

1. Which alphabet correctly describes an RNA sequence?

2. A gene annotation in a GFF file lists a feature at 1-based closed positions 5 to 8. Written as a BED interval (0-based, half-open), what are the start and end, and the length?

3. You find an N in the middle of a DNA read. What does it mean, and what should you do with it?

4. Why must you sometimes reverse-complement a sequencing read before matching it to a reference genome?

4 unanswered