The latin alphabet contains 26 characters, it is estimated that within the English language, we can create over 300,000 words with them. The alphabet in our bodies, contains only 4 characters (A,C,G,T). Together, they can create a combined total of 64 distinct words, yet those 64 words create all life we see on the planet. How is that possible?
In DNA, there is a hard rule, no word can be longer or shorter than 3 letters. Biologists call these three-letter words codons. Here is the strange part: of the 64 possible codons, only 61 actually code for something (I'll come back to the other 3). And those 61 collapse down into just 20 amino acids. The core ingredients to all life on the planet. These are known as our canonical amino acids.
I wanted to create a script where I could map all of the amino acids present in a given DNA (or RNA) sequence.
The design
One can easily find a table which maps which codon translates to which amino acid. I simply decided to hard-code this as a dictionary in my code.
Splitting up the RNA-sequence by 3 is easy, I send them all to my translator function and one by one it collects the result mapped from the dictionary.
Simple as that, I got the result back and could easily find out that leucine is the most common amino acid in most DNA-sequences. To understand why, we need to look at the table which maps codons to amino acids.
| 1st base |
2nd base | 3rd base |
|||||||
|---|---|---|---|---|---|---|---|---|---|
| U | C | A | G | ||||||
| U | UUU |
Phe Phenylalanine |
UCU |
Ser Serine |
UAU |
Tyr Tyrosine |
UGU |
Cys Cysteine |
U |
| UUC | UCC | UAC | UGC | C | |||||
| UUA |
Leu Leucine |
UCA | UAA |
Stop Ochre |
UGA |
Stop Opal |
A | ||
| UUG ⇒ | UCG | UAG |
Stop Amber |
UGG |
Trp Tryptophan |
G | |||
| C | CUU |
Leu Leucine |
CCU |
Pro Proline |
CAU |
His Histidine |
CGU |
Arg Arginine |
U |
| CUC | CCC | CAC | CGC | C | |||||
| CUA | CCA | CAA |
Gln Glutamine |
CGA | A | ||||
| CUG | CCG | CAG | CGG | G | |||||
| A | AUU |
Ile Isoleucine |
ACU |
Thr Threonine |
AAU |
Asn Asparagine |
AGU |
Ser Serine |
U |
| AUC | ACC | AAC | AGC | C | |||||
| AUA | ACA | AAA |
Lys Lysine |
AGA |
Arg Arginine |
A | |||
| AUG ⇒ |
Met Methionine |
ACG | AAG | AGG | G | ||||
| G | GUU |
Val Valine |
GCU |
Ala Alanine |
GAU |
Asp Aspartic acid |
GGU |
Gly Glycine |
U |
| GUC | GCC | GAC | GGC | C | |||||
| GUA | GCA | GAA |
Glu Glutamic acid |
GGA | A | ||||
| GUG ⇒ | GCG | GAG | GGG | G | |||||
As you can see, SIX codons map to Leucine, including ALL codons that start with the nucleotide C. The most prominent amino acid, logically it appears most often in most DNA-sequences.
Those with a keen eye will also see three codons which map to "Stop". What does this mean? Although the language of DNA contains only 64 words, it still contains gramatical rules. In the English language, we start a sentence with a capital letter, and end it with punctuation, whether it be a period, question mark, or an exclamation mark.
DNA has similar, but simplified, rules. A sentence starts with a start codon. The overwhelming majority of the time this is ATG (But it COULD also be TTG or GTG). This maps to either Methionine, Valine, or Leucine (Again!). So from this we can understand that even though a start codon initiates a DNA sentence, it also maps to an amino acid.
These sentences are stopped by (you guessed it), stop codons. These do NOT map to any amino acid, and merely function to tell the RNA polymerases (think of them as little delivery robots) where the sentence stopped. Interesting!
The code
The dictionary itself is the simplest part. Every codon just points to the amino acid it stands for, so lookups are a single step instead of a search.
codon_table = {
"UUU": "Phenylalanine",
"UUC": "Phenylalanine",
"UUA": "Leucine",
"UUG": "Leucine",
# ... all 64 codons mapped here
}
def parse(sequence):
return [sequence[i:i+3] for i in range(0, len(sequence), 3)]
def translate(codon_list):
amino_list = []
for codon in codon_list:
if codon in codon_table:
amino = codon_table[codon]
amino_list.append(amino)
return amino_list
parse() just slices the sequence into groups of three.
translate() walks that list and looks each codon up in
the table. Nothing complicated so far, but notice the
if codon in codon_table check. If a codon does not exist
in the table, it is quietly skipped instead of raising an error.
That happens more than you would think. Real sequence data is rarely a clean multiple of three, so the last group can end up as one or two leftover letters that never form a full codon. Ambiguous bases like N show up too. Right now, both cases just vanish from the output with no warning. For a first version that felt like a reasonable place to stop, but it is also the first thing on my list to fix, since silently dropping data is exactly the kind of bug that hides until it matters.
The tool also takes a --dna flag, which swaps every
T for a U before translating. Most sequence data you
pull from somewhere like NCBI is DNA, not RNA, so handling that
conversion up front made the tool far more useful than only accepting
RNA input.
Without that flag, the tool assumes the input is already RNA. That is
not an arbitrary choice. The codon table above, and the one hard-coded
into codon_table, is written in terms of U, not
T, because translation is a biological process that acts on
RNA, not DNA. A ribosome never reads DNA directly. So RNA is the
format the translator actually understands, and DNA is the format that
needs to be converted first. The --dna flag exists purely
to do that conversion before the real translation logic ever runs.
if args.dna:
sequence = sequence.upper().replace("T", "U")
codon_list = translator.parse(sequence)
amino_list = translator.translate(codon_list)
Once you have a list of amino acids, the output format is its own small decision. The full names read well in a blog post, but they are clunky if you actually want to work with the sequence afterward. So the tool supports two shorter formats as well, each with its own function.
amino_to_short = {
"Phenylalanine": "Phe",
"Leucine": "Leu",
"Serine": "Ser",
# ... one three letter code per amino acid
}
amino_to_single = {
"Alanine": "A",
"Arginine": "R",
"Asparagine": "N",
# ... one single letter code per amino acid
"Stop": "*",
}
def shorten_to_three(amino_list):
shortened_list = []
for amino in amino_list:
if amino in amino_to_short:
abbrev = amino_to_short[amino]
shortened_list.append(abbrev)
else:
shortened_list.append(amino)
return shortened_list
def shorten_to_fasta(amino_list):
shortened_list = []
for amino in amino_list:
if amino in amino_to_single:
letter = amino_to_single[amino]
shortened_list.append(letter)
else:
shortened_list.append(amino)
return shortened_list
The -s flag calls shorten_to_three, which
gives you the familiar three letter codes like Met or Leu. The
-f flag calls shorten_to_fasta, which
collapses everything down to a single letter per amino acid, matching
the standard FASTA convention used across most bioinformatics tools.
Stop is included in that single letter table too, mapped to *,
since FASTA protein sequences often mark the end of translation the
same way.
python codon-translator.py "AUGGCCUUCUAA" -f
# MAF*
python codon-translator.py "ATGGCCTTCTAA" -d -s
# Met Ala Phe Stop
Same sequence, two different ways of asking for it. The first is
already RNA and asks for FASTA formatting. The second is DNA, so it
needs the --dna flag to be converted first, and asks for
the three letter format instead.
Translating one strand start to finish is a good first step, but it quietly assumes you already know where the reading actually begins. Real DNA does not come labeled that way, and shifting the starting point by even one letter changes every codon after it. That is the question I wanted to chase next.