ROT13 short for Rotate by 13 places, is a simple substitution cipher, by shifting all letters of the alphabet 13 places forward we encode a message into something unreadable, it’s a simple form of encryption based on Caesar’s cipher that existed since 1st century BC.

For example the letter A becomes an N because we went forward 13 letters, repeat this for every character in a given string and you got basic encryption.

This encryption is not secure and so cannot be relied on anything crucial but it’s a fun way to obscure some text at a glance.

Because the alphabet is 26 letters and ROT13 shifts them by half (13 x 2 = 26) it is also an inverse of itself, meaning if we repeat the ROT13 process, shifting another half brings us back to the original, this means we don’t need to worry about decoding, we get both in one!

In this post I explore implementing it in Python and then in Rust, hoping to teach you some programming tricks on the way I hope.

Python

Let’s do this in Python first, it’s the language I started learning programming in and so I’m fairly comfortable in it, afterwards we’ll do it in Rust which is a language I’m learning, I find it helps to prototype in Python first since I can quickly get working code and understand the overall structure of the code so I can then focus on porting it to Rust.

Since there is not a lot of letters in the alphabet I figured we can just write them down and perform a lookup table as suggested on Wikipedia1

InputABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
OutputNOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm

So I started writing a function to convert those into a Python dictionary so we can map characters, for example { "A": "N", "B": "O" } and so on.

def generate_table() -> dict[str, str]:
    """
    Generates a look up table for mapping characters to their ROT13 character.
    """
    input = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    output = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm"
    lookup_table = {}

    for i, c in enumerate(input):
        lookup_table[c] = output[i]

    return lookup_table

Pretty simple, for each character in input take the equivalent index from the output and keep populating the dictionary with our final lookup table and return it.

This works and was my initial implementation but we can do better!

Since we are essentially combining two iterators we can make use of zip()!

def generate_table() -> dict[str, str]:
    """
    Generates a look up table for mapping characters to their ROT13 character.
    """
    input = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    output = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm"

    return dict(zip(input, output))

Beautiful! I really enjoy wins like this in programming where you can refactor and get some really nice and short lines of code.

With this ready, let’s write a main function to drive the logic:

import sys

def main() -> None:
    if len(sys.argv) < 2:
        print("Usage: rot13.py <string>")
        sys.exit(1)

    string = " ".join(sys.argv[1:])
    lookup_table = generate_table()
    output = ""
    
    for c in string:
        if c in lookup_table:
            output += lookup_table[c]
        else:
            output += c

    print(output)

We check and validate the user passed a command line argument and then we grab the input, skipping the first element since that’s the script name itself then we continue to build a new string with the relevant mapped characters, if there are characters that cannot be mapped such as numbers we just return the character as is.

Again, this was my initial implementation but we can do better!

Let’s use map for a one liner:

def main() -> None:
    if len(sys.argv) < 2:
        print("Usage: rot13.py <string>")
        sys.exit(1)

    string = " ".join(sys.argv[1:])
    lookup_table = generate_table()
    output = "".join(map(lambda c: lookup_table.get(c, c), string))

    print(output)

Perfect! The lambda in map takes each character and tries to look it up via dict.get, notice that get has an optional parameter for default value if the key was not found, in this case we just want the character back if it couldn’t be mapped.

Lastly we want our final __name__ check to run the script:

if __name__ == "__main__":
    main()

And we are done! Try python rot13.py Hello and you should get back Uryyb

Rust

Finally let’s tackle Rust.

First, initialize a new cargo project:

$ cargo new rot13

Open up your favorite editor on the directory and head to src/main.rs to get started!

We can do something very similar to the Python version so let’s start with generating the look up table:

use std::collections::HashMap;

fn generate_table() -> HashMap<char, char> {
    let input = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    let output = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";
    let mut lookup_table = HashMap::with_capacity(output.len());

    for (i, c) in input.chars().into_iter().enumerate() {
        lookup_table.insert(c, output.chars().nth(i).unwrap());
    }
    
    lookup_table
}

This is pretty much similar to our first python version, utilizing an enumerate loop to populate a dictionary which is now called a Hash Map in Rust.

A notable optimization we did is using HashMap::with_capacity to create the HashMap with enough capacity that we need so it’s ready to be filled in without needing to be resized internally as we fill it.

Again, we can do better, exactly like the Python version, Rust iterators also support zip() so let’s make use of it:

fn generate_table() -> HashMap<char, char> {
    let input = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    let output = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";

    input.chars().zip(output.chars()).collect()
}

Awesome! Look how clean that is, another win for clean code.

Now let’s make the main function:

use std::env;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        eprintln!("Usage: rot13 <string>");
        process::exit(1);
    }

    let input = args[1..].join(" ");
    let lookup_table = generate_table();
    let mut output = String::with_capacity(input.len());

    for c in input.chars() {
        if lookup_table.contains_key(&c) {
            output.push(lookup_table[&c]);
        } else {
            output.push(c);
        }
    }

    println!("{}", output);
}

So pretty much the same as the Python version again, mapping each character according to the lookup table, we again utilized a similar optimization for the string using String::with_capacity so it allocates the correct memory size right at the beginning.

But as you may have guessed already, we can do better!

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        eprintln!("Usage: rot13 <string>");
        process::exit(1);
    }

    let input = args[1..].join(" ");
    let lookup_table = generate_table();
    let output: String = input
        .chars()
        .map(|c| lookup_table.get(&c).copied().unwrap_or(c))
        .collect();

    println!("{}", output);
}

This makes use of iterator chaining to map it like we did in Python and finally collect a String and we are done!

Inside the map we receive the character c and we do a lookup_table.get(&c) where it returns an Option<&char> to remove the reference we did a .copied() so it’s now an Option<char> we then .unwrap_or(c) so if the option did include something we extract it otherwise it’s the character itself.

Now run it with cargo and we should see it working the same:

╰─❯ cargo run Hello
   Compiling rot13 v0.1.0 (/home/ravener/code/rust/rot13)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
     Running `target/debug/rot13 Hello`
Uryyb

Great! it works, with that our Rust implementation is also complete.

Conclusion

ROT13 is great to obscure some strings for fun, troll your friends, hide spoilers and so on, in this post we went through implementing it in Python and then in Rust and looked a bit on how we can write cleaner code to represent the idea neatly.

I enjoyed working on this problem and I thought it might be fun to write more programs like these where I try to learn a concept and write some code for it and then try to optimize / refactor it and document it in a blog post.

Lastly, can we do even better on this code? Feel free to comment below if I missed anything.

You can also find the final code in a GitHub Gist here