The Algorithms - Python Python TheAlgorithms/Python

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.

6 stops ~16 min Verified 2026-05-05
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.

1 / 6

The encrypt Signature and Docstring

ciphers/caesar_cipher.py:6

encrypt 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.

Key takeaway

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'
    """
2 / 6

The Encrypt Loop: Index, Shift, and Wrap

ciphers/caesar_cipher.py:71

The 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.

Key takeaway

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 result
3 / 6

decrypt: Reuse encrypt with a Negated Key

ciphers/caesar_cipher.py:91

decrypt 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.

Key takeaway

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)
4 / 6

brute_force: Exhaust Every Key

ciphers/caesar_cipher.py:163

brute_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.

Key takeaway

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_data
5 / 6

The __main__ Menu: Three Functions from One Loop

ciphers/caesar_cipher.py:226

The 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().

Key takeaway

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.")
            break
6 / 6

ROT13 and Why the Cipher Cannot Be Secured

ciphers/caesar_cipher.py:6

ROT13 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.

Key takeaway

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'
    """
Your codebase next

Create 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