How the Caesar Cipher Works
A modular shift over an alphabet, mirrored to decrypt, exhausted to brute-force. The oldest attested cipher in the Western record.
What you will learn
- Why modular arithmetic over the alphabet index is the complete description of the Caesar cipher
- How the encrypt function handles characters outside the alphabet by passing them through unchanged
- How decrypt reuses encrypt by negating the key rather than duplicating the shift loop
- How the brute_force function exhausts all possible keys and returns a dict of results
- Why ROT13 is the special case of shift 13 on a 26-letter alphabet
- How the interactive __main__ menu routes to all three functions from a single driver loop
Prerequisites
- Comfort reading Python string operations
- Familiarity with the modulo operator
- No prior knowledge of cryptography required
The story behind the Caesar cipher
Gaius Suetonius Tranquillus described Julius Caesar's cipher in his Lives of the Twelve Caesars (written around 121 CE): Caesar shifted each letter of his military dispatches by three positions so that a letter B would appear as E, and only his generals who knew the shift could read the message. Suetonius wrote that Caesar used this system routinely to communicate with his subordinates around 50 BCE, making it the first attested substitution cipher in the Western historical record.
The cipher is trivially broken by frequency analysis, a technique first described by the Arab polymath al-Kindi in his ninth-century manuscript A Manuscript on Deciphering Cryptographic Messages. In any language, certain letters appear far more often than others (E and T in English account for about 19% of all characters). Encrypting a long text with a Caesar cipher preserves those frequencies; the most-common letter in the cipher-text almost certainly maps to E or T, and the shift falls out immediately. Al-Kindi's insight made monoalphabetic substitution ciphers obsolete for serious cryptography roughly a thousand years ago.
Today the Caesar cipher survives as the pedagogical entry point to cryptography and as ROT13, the shift-13 variant used in Usenet and early internet forums to obscure spoilers or offensive content. Shift 13 is self-inverse on a 26-letter alphabet: applying it twice returns the original text, so encrypt and decrypt are the same function.
The encrypt Signature and Docstring
ciphers/caesar_cipher.py:6encrypt takes a string, an integer key, and an optional custom alphabet. The docstring embeds history, an example walkthrough, and three doctests.
The function signature accepts an optional alphabet parameter, which is the design choice that makes the cipher configurable rather than hard-coded to English. The docstring is unusually long for this repository because it embeds an example walkthrough: Caesar shifts H to J with key 2, then shows the full encoding of Hello, captain as Jgnnq, ecrvckp. Three doctests follow. The first uses the pangram The quick brown fox jumps over the lazy dog with key 8. The second uses a key of 8000 to demonstrate that the modulo operation wraps correctly for large keys. The third uses a custom lowercase-only alphabet to show the optional parameter in action. Each doctest runs on every push via pytest --doctest-modules.
The optional alphabet parameter makes the cipher configurable. The key-8000 doctest proves large keys wrap correctly via modulo. All three doctests run on every CI push.
def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str:
"""
encrypt
=======
Encodes a given string with the caesar cipher and returns the encoded
message
Parameters:
-----------
* `input_string`: the plain-text that needs to be encoded
* `key`: the number of letters to shift the message by
Optional:
* `alphabet` (``None``): the alphabet used to encode the cipher, if not
specified, the standard english alphabet with upper and lowercase
letters is used
Returns:
* A string containing the encoded cipher-text
More on the caesar cipher
=========================
The caesar cipher is named after Julius Caesar who used it when sending
secret military messages to his troops. This is a simple substitution cipher
where every character in the plain-text is shifted by a certain number known
as the "key" or "shift".
Example:
Say we have the following message:
``Hello, captain``
And our alphabet is made up of lower and uppercase letters:
``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ``
And our shift is ``2``
We can then encode the message, one letter at a time. ``H`` would become ``J``,
since ``J`` is two letters away, and so on. If the shift is ever too large, or
our letter is at the end of the alphabet, we just start at the beginning
(``Z`` would shift to ``a`` then ``b`` and so on).
Our final message would be ``Jgnnq, ecrvckp``
Further reading
===============
* https://en.m.wikipedia.org/wiki/Caesar_cipher
Doctests
========
>>> encrypt('The quick brown fox jumps over the lazy dog', 8)
'bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo'
>>> encrypt('A very large key', 8000)
's nWjq dSjYW cWq'
>>> encrypt('a lowercase alphabet', 5, 'abcdefghijklmnopqrstuvwxyz')
'f qtbjwhfxj fqumfgjy'
"""The Encrypt Loop: Index, Shift, and Wrap
ciphers/caesar_cipher.py:71The loop walks each character, skips non-alphabet characters unchanged, then shifts the alphabet index by key modulo the alphabet length.
Line 72 resolves the alphabet to ascii_letters if none was provided. ascii_letters from the standard library is the string abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ, which is 52 characters. The loop on line 77 walks the input character by character. Line 78 checks membership in the alphabet and passes non-alphabet characters through on line 80. Spaces, punctuation, and digits fall through unchanged, which is why the doctest output still contains spaces. The modular shift on line 83 is the entire cipher: find the character's position in the alphabet, add the key, and wrap with % len(alpha). For a 52-character alphabet a key of 8000 reduces to 8000 % 52 = 24, which is why the large-key doctest produces a short result rather than an error.
Non-alphabet characters pass through unchanged. The shift is one modular operation per character. Large keys reduce automatically via the modulo.
# Set default alphabet to lower and upper case english chars
alpha = alphabet or ascii_letters
# The final result string
result = ""
for character in input_string:
if character not in alpha:
# Append without encryption if character is not in the alphabet
result += character
else:
# Get the index of the new key and make sure it isn't too large
new_key = (alpha.index(character) + key) % len(alpha)
# Append the encoded character to the alphabet
result += alpha[new_key]
return resultdecrypt: Reuse encrypt with a Negated Key
ciphers/caesar_cipher.py:91decrypt multiplies the key by -1 and delegates to encrypt. One line does the work because subtraction is addition of the negative.
The decrypt implementation is two statements: negate the key and call encrypt. This works because the Caesar cipher's shift is linear and the modulo operator handles negative indices correctly in Python. Shifting forward by k and then backward by k returns to the original position, and % len(alpha) maps any negative index back into the valid range. The three doctests on lines 148 through 155 are the exact inverse of the encrypt doctests: the cipher-text from encrypt is the input to decrypt, and the expected output is the original plain-text. This mirrored doctest structure is the strongest possible documentation that encrypt and decrypt are true inverses of each other. Any drift in either function breaks both sets of doctests.
Decrypt is encrypt with a negated key. Python's modulo handles negative indices correctly, so the one-line delegation is correct for all inputs.
def decrypt(input_string: str, key: int, alphabet: str | None = None) -> str:
"""
decrypt
=======
Decodes a given string of cipher-text and returns the decoded plain-text
Parameters:
-----------
* `input_string`: the cipher-text that needs to be decoded
* `key`: the number of letters to shift the message backwards by to decode
Optional:
* `alphabet` (``None``): the alphabet used to decode the cipher, if not
specified, the standard english alphabet with upper and lowercase
letters is used
Returns:
* A string containing the decoded plain-text
More on the caesar cipher
=========================
The caesar cipher is named after Julius Caesar who used it when sending
secret military messages to his troops. This is a simple substitution cipher
where very character in the plain-text is shifted by a certain number known
as the "key" or "shift". Please keep in mind, here we will be focused on
decryption.
Example:
Say we have the following cipher-text:
``Jgnnq, ecrvckp``
And our alphabet is made up of lower and uppercase letters:
``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ``
And our shift is ``2``
To decode the message, we would do the same thing as encoding, but in
reverse. The first letter, ``J`` would become ``H`` (remember: we are decoding)
because ``H`` is two letters in reverse (to the left) of ``J``. We would
continue doing this. A letter like ``a`` would shift back to the end of
the alphabet, and would become ``Z`` or ``Y`` and so on.
Our final message would be ``Hello, captain``
Further reading
===============
* https://en.m.wikipedia.org/wiki/Caesar_cipher
Doctests
========
>>> decrypt('bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo', 8)
'The quick brown fox jumps over the lazy dog'
>>> decrypt('s nWjq dSjYW cWq', 8000)
'A very large key'
>>> decrypt('f qtbjwhfxj fqumfgjy', 5, 'abcdefghijklmnopqrstuvwxyz')
'a lowercase alphabet'
"""
# Turn on decode mode by making the key negative
key *= -1
return encrypt(input_string, key, alphabet)brute_force: Exhaust Every Key
ciphers/caesar_cipher.py:163brute_force tries every key from 1 to len(alphabet) and returns a dict mapping key to decrypted string. The doctest proves key 20 decodes the cipher-text.
The function iterates over every key from 1 to the alphabet length, calls decrypt for each, and stores the result in a dict keyed by the shift value. For a 52-character alphabet that is 52 calls. The first doctest picks key 20 out of the returned dict and expects the plain-text "Please don't brute force me!". The apostrophe passes through the encrypt loop unchanged because it is not in ascii_letters, which is also why the doctest output contains it verbatim. The second doctest confirms the function raises TypeError when passed an integer instead of a string, because the for loop in encrypt tries to iterate over input_string. This models the contribution checklist's requirement to test both valid and erroneous inputs.
Brute force is O(n * |alphabet|) where n is the string length. The dict return lets the caller inspect any specific key. The error doctest proves integer input is rejected.
def brute_force(input_string: str, alphabet: str | None = None) -> dict[int, str]:
"""
brute_force
===========
Returns all the possible combinations of keys and the decoded strings in the
form of a dictionary
Parameters:
-----------
* `input_string`: the cipher-text that needs to be used during brute-force
Optional:
* `alphabet` (``None``): the alphabet used to decode the cipher, if not
specified, the standard english alphabet with upper and lowercase
letters is used
More about brute force
======================
Brute force is when a person intercepts a message or password, not knowing
the key and tries every single combination. This is easy with the caesar
cipher since there are only all the letters in the alphabet. The more
complex the cipher, the larger amount of time it will take to do brute force
Ex:
Say we have a ``5`` letter alphabet (``abcde``), for simplicity and we intercepted
the following message: ``dbc``,
we could then just write out every combination:
``ecd``... and so on, until we reach a combination that makes sense:
``cab``
Further reading
===============
* https://en.wikipedia.org/wiki/Brute_force
Doctests
========
>>> brute_force("jFyuMy xIH'N vLONy zILwy Gy!")[20]
"Please don't brute force me!"
>>> brute_force(1)
Traceback (most recent call last):
TypeError: 'int' object is not iterable
"""
# Set default alphabet to lower and upper case english chars
alpha = alphabet or ascii_letters
# To store data on all the combinations
brute_force_data = {}
# Cycle through each combination
for key in range(1, len(alpha) + 1):
# Decrypt the message and store the result in the data
brute_force_data[key] = decrypt(input_string, key, alpha)
return brute_force_dataThe __main__ Menu: Three Functions from One Loop
ciphers/caesar_cipher.py:226The interactive driver loops until the user chooses 4 (Quit), routing to encrypt, decrypt, or brute_force based on the menu selection.
The __main__ block at line 226 runs a menu loop that gives access to all three functions without opening a Python shell. The input on line 232 defaults to "4" (Quit) if the user presses Enter with no input, which prevents an accidental infinite loop. The brute-force branch on lines 247 through 252 prints every key-value pair from the returned dict, so a user who intercepts a short cipher-text can scan all 52 candidates in one run. This driver is more elaborate than most algorithm files in the repository, which reflects that the Caesar cipher has three distinct operations worth exercising interactively. The contribution guide requires .strip() on any input() call that feeds into a conversion; both key reads on lines 239 and 244 use .strip() before int().
The menu loop exposes all three functions interactively. Defaulting to Quit on empty input prevents an unintended loop. Both key reads strip whitespace before converting to int.
if __name__ == "__main__":
while True:
print(f"\n{'-' * 10}\n Menu\n{'-' * 10}")
print(*["1.Encrypt", "2.Decrypt", "3.BruteForce", "4.Quit"], sep="\n")
# get user input
choice = input("\nWhat would you like to do?: ").strip() or "4"
# run functions based on what the user chose
if choice not in ("1", "2", "3", "4"):
print("Invalid choice, please enter a valid choice")
elif choice == "1":
input_string = input("Please enter the string to be encrypted: ")
key = int(input("Please enter off-set: ").strip())
print(encrypt(input_string, key))
elif choice == "2":
input_string = input("Please enter the string to be decrypted: ")
key = int(input("Please enter off-set: ").strip())
print(decrypt(input_string, key))
elif choice == "3":
input_string = input("Please enter the string to be decrypted: ")
brute_force_data = brute_force(input_string)
for key, value in brute_force_data.items():
print(f"Key: {key} | Message: {value}")
elif choice == "4":
print("Goodbye.")
breakROT13 and Why the Cipher Cannot Be Secured
ciphers/caesar_cipher.py:6ROT13 is the shift-13 special case. Any Caesar cipher is broken by frequency analysis because the letter distribution of the plain-text survives the shift.
ROT13 is encrypt(text, 13) on a 26-letter alphabet. Because 13 is exactly half of 26, applying ROT13 twice returns the original text. That self-inverse property made it the standard spoiler-hiding convention on Usenet and early internet forums: calling encrypt with key 13 both encrypts and decrypts. On the 52-character alphabet this file uses by default (upper and lower case), ROT13 does not produce a self-inverse because 13 is not half of 52. The cipher is pedagogically important precisely because it fails so completely as cryptography. The modular shift preserves letter frequencies, and al-Kindi's ninth-century frequency analysis attack breaks any Caesar cipher on a text of a few hundred characters. The brute_force function in this file makes that even clearer: 52 candidates is an exhaustive search that runs in milliseconds.
ROT13 is shift-13 on a 26-letter alphabet, self-inverse because 13 + 13 = 26. Frequency analysis breaks any Caesar cipher; brute_force here confirms 52 candidates is trivial.
def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str:
"""
encrypt
=======
Encodes a given string with the caesar cipher and returns the encoded
message
Parameters:
-----------
* `input_string`: the plain-text that needs to be encoded
* `key`: the number of letters to shift the message by
Optional:
* `alphabet` (``None``): the alphabet used to encode the cipher, if not
specified, the standard english alphabet with upper and lowercase
letters is used
Returns:
* A string containing the encoded cipher-text
More on the caesar cipher
=========================
The caesar cipher is named after Julius Caesar who used it when sending
secret military messages to his troops. This is a simple substitution cipher
where every character in the plain-text is shifted by a certain number known
as the "key" or "shift".
Example:
Say we have the following message:
``Hello, captain``
And our alphabet is made up of lower and uppercase letters:
``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ``
And our shift is ``2``
We can then encode the message, one letter at a time. ``H`` would become ``J``,
since ``J`` is two letters away, and so on. If the shift is ever too large, or
our letter is at the end of the alphabet, we just start at the beginning
(``Z`` would shift to ``a`` then ``b`` and so on).
Our final message would be ``Jgnnq, ecrvckp``
Further reading
===============
* https://en.m.wikipedia.org/wiki/Caesar_cipher
Doctests
========
>>> encrypt('The quick brown fox jumps over the lazy dog', 8)
'bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo'
>>> encrypt('A very large key', 8000)
's nWjq dSjYW cWq'
>>> encrypt('a lowercase alphabet', 5, 'abcdefghijklmnopqrstuvwxyz')
'f qtbjwhfxj fqumfgjy'
"""You've walked through 6 key areas of the The Algorithms - Python codebase.
Continue: How RSA Encryption Works → Browse all projectsCreate code tours for your project
Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.
Install Intraview Free