← Back to Home

From Codon to Amino

Posted on [12-07-2026] | Category: Bioinformatics | Source code


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.


RNA Codon table
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
Nonpolar
Polar
Basic
Acidic
Stop codon
Start codon (⇒)

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.