Several ways to use ROT13 letter substitution cipher in Python 3

Alan Wang
6 min readJan 6, 2021

--

Photo by Alex Motoc on Unsplash

ROT13 (“rotate by 13 places”) is a simple letter substitution cipher, a special case of “Caesar cipher” and was once popular on Internet forums.

The rule is very simple: you replace each letters with the 13th letter behind them. If the range is over the last letter z, just loop back and continue to count from letter a. Since there are 26 letters in English, apply ROT13 twice (or 4, 6, 8 times…) will return the original text:

ROT13("Python") -> "Clguba"
ROT13("Clguba") -> "Python"

Although The Zen of Python said “There should be one — and preferably only one — obvious way to do it”, there are in fact many ways to implement this in Python 3. This simple article will explore some of these methods.

The straightforward way — — using ASCII index

Photo by Nick Hillier on Unsplash

The first method is to iterate the word using ord() to get the Unicode of each letter in a word (for a~z they are the same as ASCII code), calculate the new value and use chr() to convert it back to letters.

To simply things, we assume our function rot13() would only receive a single word which only consisted of a~z or A~z letters. ord(‘a’) gets 97, and chr(97) gets ‘a’. The result will be a lower case word no matter what.

def rot13(word):
return ''.join([chr((ord(letter) - 97 + 13) % 26 + 97)
for letter in word.lower()])
print(rot13('python'))

The list comprehension would generates a list of converted letters, then use str.join() to combine them together into a single string without any separation characters.

So we submit a word “python” and will get the following output:

clguba

Let’s expand this a bit so it can process a longer sentence with multiple words:

def rot13(sentence):
return ''.join([chr((ord(letter) - 97 + 13) % 26 + 97)
if 97 <= ord(letter) <= 122
else letter
for letter in sentence.lower()])
print(rot13('python programming language!'))

If a letter does not have Unicode between 97 (‘a’) and 122 (‘z’), it would be added without changing.

This will get you

clguba cebtenzzvat ynathntr!

The Zen of Python way — — using dictionary

Photo by Kari Shea on Unsplash

Most Python learners probably know that you can read “The Zen of Python” by executing “import this” in REPL:

>>> import thisThe Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
...

Fewer people would also know that the source code of “this” is not so straightforward — — the text is entirely coded in ROT13 and then converted back to normal English:

>>> with open(this.__file__) as f:
... print(f.read())
s = """Gur Mra bs Clguba, ol Gvz Crgref

Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
..."""

d = {}
for c in (65, 97):
for i in range(26):
d[chr(i+c)] = chr((i+13) % 26 + c)

print("".join([d.get(c, c) for c in s]))

(So even The Zen of Python does something not beautiful, not explicit and not simple in Python. Funny, isn’t it?)

Let’s define a rot13() function based on the coding method above:

def rot13(sentence):
d = {}
for ascii_code in (65, 97):
for alphabet_index in range(26):
d[chr(ascii_code + alphabet_index)] = \
chr(ascii_code + (alphabet_index + 13) % 26)
return ''.join([d.get(letter, letter) for letter in sentence])
print(rot13('Python programming language!'))

Here the code first builds a dictionary d as a mapping table: d[‘A’]=‘N’, d[‘B’]=‘O’, d[‘C’]=’P’, and d[‘a’]=‘n’, d[‘b’]=‘o’, etc. Then it uses list comprehension to look up letters from it. (dict.get() returns the letter itself if it is not found in the dictionary; in other words, non-alphabet characters will be unchanged.)

Using string for index and mapping instead

Photo by Maxim Fiyavchuk on Unsplash

Here’s another way to convert letters by using string as mapping table instead of a dictionary (this is based on code found here). Just the same, in order to simplify things, this rot13() would only returns lower case results.

import stringdef rot13(sentence):
alphabet = string.ascii_lowercase
return ''.join([alphabet[(alphabet.find(letter) + 13) % 26]
if alphabet.find(letter) >= 0 else letter
for letter in sentence.lower()])
print(rot13('python programming language!'))

“string.ascii_lowercase” returns a string = ‘abcde…xyz’. We use str.find() to locate the index of each letter, calculate the new positions and get the new letters. If the letter is not found in the string (get index -1, hence not an alphabet), use the original letter instead.

Using str.maketrans() and str.translate()

Photo by Edurne Chopeitia on Unsplash

There’s yet another way to make a mapping table in Python. (This is based on code found here.)

def rot13(sentence):
rot13 = str.maketrans(
'abcdefghijklmnopqrstuvwxyz',
'nopqrstuvwxyzabcdefghijklm'
)
return str.translate(sentence.lower(), rot13)
print(rot13('python programming language!'))

The code above is only applicable for lower case sentences. If we want to use this for both upper and lower case letters and not having to input all the alphabets twice, we can modify the code like this:

import stringdef rot13(sentence):
alphabet_l = string.ascii_lowercase
alphabet_u = string.ascii_uppercase
rot13 = str.maketrans(
alphabet_l + alphabet_u,
alphabet_l[13:] + alphabet_l[:13] + \
alphabet_u[13:] + alphabet_u[:13]
)
return str.translate(sentence, rot13)
print(rot13('PYTHON Programming Language!'))

Now we have a better mapping table, and the result is

CLGUBA Cebtenzzvat Ynathntr!

The fastest and easiest way — — using codecs

Photo by Harley-Davidson on Unsplash

For all the efforts above, the codecs module already supports ROT13 encoding, which is surprisingly simple:

import codecsdef rot13(sentence):
return codecs.encode(sentence, 'rot_13')
print(rot13('PYTHON Programming Language'))

Now, let’s try to use codecs.encode() on Zen of Python’s attribute s, which contains the ROT13 version of the text. Here we temporarily block/overwrite Python’s print() so module “this” won’t print the result on import, and “decode” the ROT13 string:

import sys, ossys.stdout = open(os.devnull, 'w')  # block print()
import this
sys.stdout = sys.__stdout__ # enable print()
import codecs
print(codecs.encode(this.s, 'rot_13'))

We will get the output

The Zen of Python, by Tim PetersBeautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
...

I’ll stop here and leave you to contemplate about the meanings of life, the universe and everything as well as why Python itself sometimes make things complicated.

Photo by Jared Rice on Unsplash

--

--

Alan Wang
Alan Wang

No responses yet