Skip to content
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after any top-up. Until Nov 30.

How it works

Caesar Cipher in Python: Encrypt and Decrypt Text Step by Step

Build a Caesar cipher in Python, test it with unittest, and see why this shift cipher gives no real secrecy.

Beginner5 min readBy NotHoly

What you will build

You will write a caesar() function. It shifts English letters by a number of positions, leaves every other character alone, and reverses the shift when you pass a negative number. Then you will run a small test suite and read what it checks.

You need Python 3 and nothing else. If the idea of a shift cipher is new, read What Is the Caesar Cipher? first.

Before you start

Install Python 3 from python.org. The code uses only the standard library, so there is nothing to install with pip. Create a folder for the lab and save the files there.

The output in this article came from Python 3.12.10 on Windows 11. I did not test other versions.

Step 1: letters as numbers

Python's ord() returns the Unicode code point of a character. chr() does the reverse and turns a code point back into a character. For the uppercase letter A, the code point is 65.

Run this in a terminal:

bash
python -I -c "print(ord('A'), chr(65))"

Output:

text
65 A

The -I flag runs Python in isolated mode. Environment variables that start with PYTHON are ignored, and the current folder is not added to the module search path. Use it when you run code from a folder you did not write yourself.

A shift works on a zero-based position. Subtracting 65 turns A into 0 and Z into 25. The % 26 step wraps the result back into the alphabet, and adding 65 restores the uppercase range:

python
chr((ord(letter) - 65 + shift) % 26 + 65)

Lowercase letters start at 97, so the same idea uses 97 instead of 65.

Python's % operator never returns a negative number when the divisor is positive. That is why a negative shift works without extra code:

bash
python -I -c "print((0 - 3) % 26)"

Output:

text
23

Zero minus three wraps to 23, which is the position of X.

Step 2: write the cipher

Save this as caesar_cipher.py. It is the same code as the lab file.

python
"""Educational Caesar cipher only. NOT secure encryption.

Shift ASCII English A-Z/a-z; preserve every other Unicode character as-is.
"""

def caesar(text: str, shift: int) -> str:
    shift %= 26
    result = []
    for character in text:
        if 'A' <= character <= 'Z':
            result.append(chr((ord(character) - 65 + shift) % 26 + 65))
        elif 'a' <= character <= 'z':
            result.append(chr((ord(character) - 97 + shift) % 26 + 97))
        else:
            result.append(character)
    return ''.join(result)

if __name__ == '__main__':
    print(caesar('HELLO, World!', 3))  # KHOOR, Zruog!
    print(caesar('KHOOR, Zruog!', -3)) # HELLO, World!

The first line, shift %= 26, reduces any shift to the range 0 to 25. A shift of 29 behaves like a shift of 3, and a shift of -3 becomes 23.

Run the file:

bash
python -I caesar_cipher.py

Output:

text
KHOOR, Zruog!
HELLO, World!

The function changes only the letters A to Z and a to z. Digits, spaces, punctuation and accented letters pass through unchanged. That keeps the code short. A cipher that handled accented letters would need a rule for each of them, and this lab does not define one.

Step 3: run the tests

The lab also includes test_caesar_cipher.py. Open a terminal in the folder that holds both files and run:

bash
python -I -m unittest discover -p 'test_*.py' -v

Output:

text
test_large_shift (test_caesar_cipher.CaesarCipherTest.test_large_shift) ... ok
test_lower (test_caesar_cipher.CaesarCipherTest.test_lower) ... ok
test_negative (test_caesar_cipher.CaesarCipherTest.test_negative) ... ok
test_roundtrip (test_caesar_cipher.CaesarCipherTest.test_roundtrip) ... ok
test_upper (test_caesar_cipher.CaesarCipherTest.test_upper) ... ok
test_wrap (test_caesar_cipher.CaesarCipherTest.test_wrap) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

Each test checks one behavior:

  • test_upper and test_lower confirm that HELLO becomes KHOOR and attack becomes dwwdfn.
  • test_wrap confirms that Z moves forward to C, which only works if the wraparound is correct.
  • test_large_shift confirms that a shift of 29 gives the same result as a shift of 3.
  • test_negative checks decryption by shifting DEF by -3.
  • test_roundtrip encrypts text with a shift of 7, decrypts it with -7, and expects the original back. The sample text includes punctuation, a digit and á, so it also checks that non-letters survive.

If you change the code and a test fails, the failure message shows the input, the expected value and the actual value. Read that message before you change anything else.

Step 4: understand the limit

A Caesar cipher has only 25 useful shifts. Shift 0 and shift 26 both return the original text. A person can try every shift by hand, and a program can try them all in an instant.

Frequency analysis is a second problem. In English, letters such as E and T appear more often than Q or Z. On a longer message, the most common letters in the ciphertext often point to the most common letters in the plaintext.

So this cipher provides no real confidentiality. Do not use it for passwords, customer records, tokens or anything else that matters. If you need real encryption, use a well-tested library and a standard algorithm. The OWASP Cryptographic Storage Cheat Sheet lists the basic rules for protecting stored data.

Treat this exercise as a way to learn modulo arithmetic, character codes and the difference between a toy transformation and a real cipher.

Optional: review the code with an AI model

After the tests pass, you can ask a language model to explain the modulo line, suggest edge cases, or write more tests. A useful prompt names the limits up front:

Review this educational Caesar cipher. What happens with negative shifts, accented characters and non-Latin text? Do not describe it as secure encryption.

Run any test the model proposes before you accept it. Models make mistakes, and a test that fails tells you more than a confident explanation.

Next step

Once the lab works, keep the folder in version control so you can track changes. The next article, What Is GitHub?, explains how to do that.

Sources

Sources

  1. Python built-in functions (ord, chr)
  2. Python unittest documentation
  3. Python command-line options (-I)
  4. OWASP Cryptographic Storage Cheat Sheet

More in Cryptography

Cryptography

Passwords and Hashing with Argon2id

Why fast hashes fail for passwords, what salts and work factors do, and how to hash and verify with Argon2id in Python using argon2-cffi.

Intermediate10 min read