A Caesar cipher is a letter for letter exchange from a clear text to a encrypted text by shifting a set distance in the alphabet. Thus, if your set distance is 2, the letter A becomes C, the letter X becomes Z. To encrypt the last letters of the alphabet, you wrap around to the beginning, so Y becomes A and Z becomes B.
Caesar is commonly respelled to Ceasar, which I have done for this article. Use the spelling of your choice.
This is a fairly easy algorithm to code, and makes for a decent early class in python. Here are the steps I would go through to teach someone:
I would do this on Linux, of course, using vim, but that is my problem. I won’t go into editor usage.
Start by writing and executing a hello_world program:
vim ceasar.py:
#!/bin/python3 print("OK") |
Make the file executable.
chmod +x ceasar.py
Run the program.
./ceasar.py
OK
Always work from success. Don’t try to do more until you can get this far. Make sure you understand what you have done.
The Line !/bin/python3 tells the you that this a script to be executed by the python interpreter. The #! will be read by the Linux Kernel when you execute the program. There are lots of magic numbers for different file types. This one tells Linux to treat the file as text, to start reading until a newline, and to execute the program named by that string, with the rest of the file contents fed into that interpreter. This is pretty complex stuff, and expect people to either zone out or ask lots of questions. You could, if necessary show a different scripting language, such as bash or ruby.
The print(“OK”) is a function. That function is responsible for the OK you see on the screen after running the program.
Once you get this far, you might want to have the students change the text from OK to Hello or something, so they can see how they affect change, and get feedback from coding.
Now run
git init.
git add ceasar.py
git commit -m "Hello World"
Yeah, git. This will allow them to reset themselves later. Answering questions about this will probably kill the rest of the class session. But using git is fundamental to not losing your mind as a developer.
Next we are going to give them an input text. For this example, I will use the opening line from Pride and Prejudice:
“is, “It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.”
Jane Austen from PRide and PRejudice
The code should now look like this:
!/bin/python3
plain_text=”It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.”
print(plain_text)
This introduces the concept of variables. plain_text is a variable that uses the snake_case naming convention. The underscore allows you to separate words while telling python that the whole collection of characters is one variable name.
Run it.
./ceasar.py
It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.
Add to git and commit:
git add ceasar.py
git commit -m "plain text"
Now lets uppercase the whole thing.
The code will look like this:
#!/bin/python3 plain_text="It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife." print(plain_text.upper()) |
And the difference from the previous code can be shown with git diff
git diff
diff --git a/ceasar.py b/ceasar.py
index 76c6294..388e25f 100755
--- a/ceasar.py
+++ b/ceasar.py
@@ -3,4 +3,4 @@
plain_text="It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife."
-print(plain_text)
+print(plain_text.upper())
The output looks like this:
$ ./ceasar.py
IT IS A TRUTH UNIVERSALLY ACKNOWLEDGED, THAT A SINGLE MAN IN POSSESSION OF A GOOD FORTUNE, MUST BE IN WANT OF A WIFE.
Add to git and commit:
git add ceasar.py
git commit -m "to upper"
We are trying to establish the good habit of capturing your successes.
Lets go through the plain text letter by letter, now.
#!/bin/python3 plain_text="It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife." encrypted_text = "" for letter in plain_text.upper(): encrypted_text += letter print(encrypted_text) |
Which will look like this when executed:
/ceasar.py
IT IS A TRUTH UNIVERSALLY ACKNOWLEDGED, THAT A SINGLE MAN IN POSSESSION OF A GOOD FORTUNE, MUST BE IN WANT OF A WIFE.
i.e. exactly the same as before. We might have fooled ourselves. But the next change is going to be an important step in the understanding of coding Lets do a change that shows we actually made things work. Commit to git before continuing.
Lets strip out all characters that are not A-Z.
git add ceasar.py
git commit -m "letter by letter"
Now we will strip out all non-Alphabet characters. Make the following change. The – at the start of the line means match and remove that line, replacing it with the lines below it that start with +. Do not include the + or – characters that are at the start of the line.
- encrypted_text += letter + if (letter >= 'A' and letter <= 'Z'): + encrypted_text += letter |
Now your code should look like this:
#!/bin/python3 plain_text="It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife." encrypted_text = "" for letter in plain_text.upper(): if (letter >= 'A' and letter <= 'Z'): encrypted_text += letter print(encrypted_text) |
And a run of the program looks like this
./ceasar.py
ITISATRUTHUNIVERSALLYACKNOWLEDGEDTHATASINGLEMANINPOSSESSIONOFAGOODFORTUNEMUSTBEINWANTOFAWIFE
Note that now we need to mentally put the spaces back in to read it. This is a shortcoming of the Ceasar cipher, and it is something we can address with more complex cpihers in the future.
Add to git in a similar manner to how I wrote earlier.
adam@standard:~/devel/letterfreq$ git add ceasar.py
adam@standard:~/devel/letterfreq$ git commit -m "letters only"
Note how the only thing that differs on each of these commits is the message. At this point, we should have a few. We can see them (from newest to oldest) using git log:
git log
commit 422eed0a5e0c5a0b30855a3a84ae7c77fbe3945b (HEAD -> main)
Author: Adam Young <adam@younglogic.com>
Date: Mon Jul 27 15:15:52 2026 -0400
letters only
commit af884dcb08921deddfca70bcde548762310c3dc0
Author: Adam Young <adam@younglogic.com>
Date: Mon Jul 27 15:05:14 2026 -0400
letter by letter
commit 6156eaeafcaf7054044aca41aebf8f8bfe456172
Author: Adam Young <adam@younglogic.com>
Date: Mon Jul 27 14:57:21 2026 -0400
to upper
commit d739450b2c89f7e566d36f5ff201d2807bd72346
Author: Adam Young <adam@younglogic.com>
Date: Mon Jul 27 14:54:16 2026 -0400
plain text
commit 58477d16f983d2d2a32c6e4ba9769313d774b644
Author: Adam Young <adam@younglogic.com>
Date: Mon Jul 27 14:49:01 2026 -0400
hello world
Now we will convert from the letters A to Z to their numerical equivalent. We are using an encoding scheme called ASCII (American Standard Code for Information Interchange) that maps ‘A’ to the number 65. The rest of the alphabet follows in standard order: B=66, C=67 and so on. So to convert, we use the ord function (short for ordinal).
To convert ‘A’ to 65, we would write
val = ord(‘A’)
Lets do only that and see what we get. Yeah, this is going to corrupt our output, but it will be illuminating. After the line
encrypted_text += letter
add these lines
val = ord(letter)
print(f"ordinal = {val}")
This will print out a lot of output, so much that it will scroll off the screen. The last few lines look like this:
ordinal = 87
ordinal = 73
ordinal = 70
ordinal = 69
ITISATRUTHUNIVERSALLYACKNOWLEDGEDTHATASINGLEMANINPOSSESSIONOFAGOODFORTUNEMUSTBEINWANTOFAWIFE
We won’t commit this to git, as it is a broken stage. Lets instead do some arithmatic.
In order to perform the portion of the Ceasar cipher that wraps around, we want to use the arithmetic operation of modulus. This is the remainder function of division. Since there are 26 letters, the number 0 through 25 are returned unchanged, but any number larger than 25 will instead return a number 0-25. In python, this is the % operator. You might want to show this in a stand alone fashion.
Note that I am going to run the python interpreter from the command line to show this.
$ python3
Python 3.14.4 (main, Jun 18 2026, 14:25:02) [GCC 15.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> ord('A')
65
>>> 25 % 26
25
>>> 27 % 26
1
>>> ord('A') % 26
13
However, we don’t want to convert ‘A’ to 13. So, we are going to convert ‘A” from 65 to 0, B to 1, and so on. We do this by subtracting the ord value of ‘A’ from each letter:
>>> ord('A') % 26
13
>>> ord ('A') - ord('A')
0
>>> ord ('B') - ord('A')
1
>>> ord ('C') - ord('A')
2
>>> ord ('Z') - ord('A')
Now we can perform cipher change. For example, if we wanted to encrypt Z by 13:
>>> (ord ('Z') - ord('A') + 13) % 26
12
To convert this back to a character, add the ord value of ‘A” and use the chr function.
>>> chr(12 + ord ('A'))
'M'
To exit the interpreter, run the quit function like this
quit()
Lets add this logic to our code. IT should look like this.
#!/bin/python3 #!/bin/python3 plain_text="It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife." encrypted_text = "" key = 13 for letter in plain_text.upper(): if (letter >= 'A' and letter <= 'Z'): plain_val = ord(letter) - ord('A') encrypted_val = (plain_val + key) % 26 encrypted_text += chr(encrypted_val + ord('A')) print(encrypted_text) ~ |
Note that the function is chr, and not char. char is a key word in python, and the error message might be a bit hard to debug if you accidentally type that instead.
Note also the use of parenthesis to handle the order of operations. You want the modulus of the value after you subtract 65. If you were to try and execute
encrypted_val = plain_val + key % 26
Python would first perform key % 26 and then add plain_val which would not be correct.
Running the above code should look like this:
./ceasar.py
VGVFNGEHGUHAVIREFNYYLNPXABJYRQTRQGUNGNFVATYRZNAVACBFFRFFVBABSNTBBQSBEGHARZHFGORVAJNAGBSNJVSR
adam@standard:~/devel/letterfreq$ git add ceasar.py
adam@standard:~/devel/letterfreq$ git commit -m "encrypt"
Now we want to make it easier to encrypt text without changing our program. We will read from standard input instead of a constant string. To do this, we first need to import the standard library called sys.
import sys
We can remove our Jane Austen quote and add an outer loop
for line in sys.stdin:
# Use .rstrip() to remove the trailing newline character
plain_text = line.rstrip()
One of the biggest pains in python is that white space, especially tab characters, are significant. We need to move all of the internal loop code one more indentation to the left
Now the overall code should look like this:
#!/bin/python3 import sys plain_text="It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife." encrypted_text = "" key = 13 for line in sys.stdin: # Use .rstrip() to remove the trailing newline character plain_text = line.rstrip() for letter in plain_text.upper(): if (letter >= 'A' and letter <= 'Z'): plain_val = ord(letter) - ord('A') encrypted_val = (plain_val + key) % 26 encrypted_text += chr(encrypted_val + ord('A')) print(encrypted_text) |
You can now use the Linux cat utility to test your program. I have a couple paragraphs from the start of “Zen and the Art of Motorcycle Maintenance” that I can encrypt like this:
cat zen.txt | ./ceasar.py
VPNAFRROLZLJNGPUJVGUBHGGNXVATZLUNAQSEBZGURYRSGTEVCBSGURPLPYRGUNGVGVFRVTUGGUVEGLVAGURZBEAVATGURJVAQRIRANGFVKGLZVYRFNAUBHEVFJNEZNAQUHZVQJURAVGFGUVFUBGNAQZHTTLNGRVTUGGUVEGLVZJBAQREVATJUNGVGFTBVATGBORYVXRVAGURNSGREABBAVAGURJVAQNERCHATRAGBQBEFSEBZGURZNEFURFOLGUREBNQJRNERVANANERNBSGURPRAGENYCYNVAFSVYYRQJVGUGUBHFNAQFBSQHPXUHAGVATFYBHTUFURNQVATABEGUJRFGSEBZZVAARNCBYVFGBJNEQGURQNXBGNFGUVFUVTUJNLVFNABYQPBAPERGRGJBYNAREGUNGUNFAGUNQZHPUGENSSVPFVAPRNSBHEYNAREJRAGVACNENYYRYGBVGFRIRENYLRNEFNTBJURAJRCNFFNZNEFUGURNVEFHQQRAYLORPBZRFPBBYREGURAJURAJRNERCNFGVGFHQQRAYLJNEZFHCNTNVAVZUNCCLGBOREVQVATONPXVAGBGUVFPBHAGELVGVFNXVAQBSABJURERSNZBHFSBEABGUVATNGNYYNAQUNFNANCCRNYORPNHFRBSWHFGGUNGGRAFVBAFQVFNCCRNENYBATBYQEBNQFYVXRGUVFJROHZCNYBATGURORNGHCPBAPERGRORGJRRAGURPNGGNVYFNAQFGERGPURFBSZRNQBJNAQGURAZBERPNGGNVYFNAQZNEFUTENFFURERNAQGURERVFNFGERGPUBSBCRAJNGRENAQVSLBHYBBXPYBFRYLLBHPNAFRRJVYQQHPXFNGGURRQTRBSGURPNGGNVYFNAQGHEGYRFGURERFNERQJVATRQOYNPXOVEQ
One thing about using a key of 13 is that it can decrypt just by encrypting a second time. Thus, if I run my encrypted text through the cipher, I should get my clear text:
$ cat zen.txt | ./ceasar.py | ./ceasar.py
ICANSEEBYMYWATCHWITHOUTTAKINGMYHANDFROMTHELEFTGRIPOFTHECYCLETHATITISEIGHTTHIRTYINTHEMORNINGTHEWINDEVENATSIXTYMILESANHOURISWARMANDHUMIDWHENITSTHISHOTANDMUGGYATEIGHTTHIRTYIMWONDERINGWHATITSGOINGTOBELIKEINTHEAFTERNOONINTHEWINDAREPUNGENTODORSFROMTHEMARSHESBYTHEROADWEAREINANAREAOFTHECENTRALPLAINSFILLEDWITHTHOUSANDSOFDUCKHUNTINGSLOUGHSHEADINGNORTHWESTFROMMINNEAPOLISTOWARDTHEDAKOTASTHISHIGHWAYISANOLDCONCRETETWOLANERTHATHASNTHADMUCHTRAFFICSINCEAFOURLANERWENTINPARALLELTOITSEVERALYEARSAGOWHENWEPASSAMARSHTHEAIRSUDDENLYBECOMESCOOLERTHENWHENWEAREPASTITSUDDENLYWARMSUPAGAINIMHAPPYTOBERIDINGBACKINTOTHISCOUNTRYITISAKINDOFNOWHEREFAMOUSFORNOTHINGATALLANDHASANAPPEALBECAUSEOFJUSTTHATTENSIONSDISAPPEARALONGOLDROADSLIKETHISWEBUMPALONGTHEBEATUPCONCRETEBETWEENTHECATTAILSANDSTRETCHESOFMEADOWANDTHENMORECATTAILSANDMARSHGRASSHEREANDTHEREISASTRETCHOFOPENWATERANDIFYOULOOKCLOSELYYOUCANSEEWILDDUCKSATTHEEDGEOFTHECATTAILSANDTURTLESTHERESAREDWINGEDBLACKBIRD
Or if we use Pride and Prejudice:
cat pride-prejudice.txt | ./ceasar.py | ./ceasar.py
ITISATRUTHUNIVERSALLYACKNOWLEDGEDTHATASINGLEMANINPOSSESSIONOFAGOODFORTUNEMUSTBEINWANTOFAWIFE
git add ceasar.py
git commit -m "encrypt from the command line"