Build a Vigenère Cipher in Python: A Beginner Coding Project
In 1553 Giovan Battista Bellaso published a cipher that shifted each letter of a message by a different amount, driven by a repeating keyword. Blaise de Vigenère later gave it a fuller treatment and, unfairly, his name. For three centuries it resisted practical attack, earning the nickname le chiffre indéchiffrable, until Friedrich Kasiski published a general method for breaking it in 1863. Writing one in Python takes about thirty lines. Breaking one takes sixty. Doing both is the most instructive afternoon you can spend on classical cryptography.
How the Vigenère Cipher Works
A Caesar cipher shifts every letter by the same amount, so one letter always becomes the same other letter. That predictability is its weakness. The Vigenère cipher keeps the arithmetic but makes the shift depend on position: the keyword repeats across the message, and its letters supply a different shift for each character.
Number the alphabet from 0 (A) to 25 (Z). Then the rule is simply:
cipher_value = (plain_value + key_value) mod 26
A worked example
Encrypt ATTACKATDAWN with the key LEMON, repeating the key as far as the message needs it:
- A + L = 0 + 11 = 11 → L
- T + E = 19 + 4 = 23 → X
- T + M = 19 + 12 = 31 mod 26 = 5 → F
- A + O = 0 + 14 = 14 → O
- C + N = 2 + 13 = 15 → P
- K + L = 10 + 11 = 21 → V
Continuing to the end gives LXFOPVEFRNHR. Notice what happened to the two T's: they became X and F. The same plaintext letter produced different ciphertext letters because it sat under different key letters. That is the entire trick, and it is why a simple letter count over the whole message tells you almost nothing.
Setting Up the Project
You need Python 3 and a text editor. No packages, no virtual environment, no configuration. Create one file called vigenere.py and put both the cipher functions and the cracker in it, or split the cracker into a second file that imports from the first.
Most beginner bugs in this project come from undecided conventions rather than tricky logic, so settle these first:
- Keys contain letters only. Strip spaces and digits, and reject an empty key.
- Spaces and punctuation pass through untouched and do not consume key letters. This keeps the text readable and keeps encryption and decryption symmetrical.
- Preserve the original case in the output, but do the arithmetic on uppercase values internally.
- Advance the key position only when you actually consume a letter.
Encryption and Decryption in One Function
Write a single function that takes the text, the key, and a mode flag. Encryption and decryption differ by one sign, so duplicating the loop only invites drift between the two.
Walk the message one character at a time. If the character is not alphabetic, append it unchanged and move on. Otherwise compute the shift from the current key letter using ord(key_letter) - ord('a'), negate it when decrypting, and pick the base value from the character's case — ord('a') for lowercase, ord('A') for uppercase. Then build the new character:
new_char = chr((ord(ch) - base + shift) % 26 + base)
Python's modulo operator returns a positive result even for negative inputs, so decryption needs no special handling. Pull the key letter with key[key_index % len(key)] and increment the counter after each letter. Then add a main block that reads text, key, and mode from the terminal, plus a known-answer test: encrypting "ATTACKATDAWN" with "LEMON" must return "LXFOPVEFRNHR". If that assertion passes, your arithmetic is almost certainly right.
Thinking Like a Cracker
The cipher hides single-letter frequencies, but it does not destroy them. It smears them across the message. If the key is LEMON, then every letter at position 0, 5, 10 and so on was shifted by L. Every letter at position 1, 6, 11 was shifted by E.
That gives you the attack. Guess the key length, cut the ciphertext into that many columns, and each column is an ordinary Caesar cipher with the usual lopsided letter distribution. Solve each column and you have the key. Every Vigenère cracker, from pencil-and-paper to Python, rests on that observation.
Two classic tools help with the length. Kasiski's method looks for repeated sequences: repeats in the ciphertext usually mean repeats in the plaintext, and the distance between them tends to be a multiple of the key length. The index of coincidence measures how unevenly letters are spread — English text scores around 0.066, random text around 0.038. A correct key length makes each column look like English.
Building the Frequency-Analysis Cracker
The plan is brute force plus scoring:
- Clean the ciphertext to uppercase letters only.
- For each candidate length from 1 to about 12, split the letters into columns by index modulo that length.
- Score every column against English letter frequencies and total the scores.
- Take the length with the best total, then find the best Caesar shift for each of its columns.
- Map those shifts back to letters to get the key, decrypt, and check the result.
Scoring with chi-squared
Keep a rough English frequency table: E about 12.7%, T 9.1, A 8.2, O 7.5, I 7.0, N 6.7, S 6.3, H 6.1, R 6.0, and the rest tapering down through D, L, C, U, M, W, F, G, Y, P, B, V, K to J, X, Q and Z under one percent each. These figures are approximations, but they are close enough for the job.
For a column of N letters and a candidate shift, count how often each letter appears after shifting, compute an expected count of N times that letter's frequency, and sum (observed - expected) ** 2 / expected across the alphabet. Lower totals are better. The shift with the lowest total is your guess for that column.
Two practical notes. Columns of a hundred or more letters crack quickly; columns of fifteen are noisy and may mislead you. And if the winning key length looks arbitrary on a short message, you probably need more ciphertext rather than a cleverer algorithm.
Recovering the key
Each column's best shift maps back to a key letter: 0 becomes A, 11 becomes L, and so on. Decrypt with your own function and read the output. If the text is nearly readable but not quite, try the second-best shift on the columns whose scores were close together. English letter frequencies describe the language on average, and short samples wander.
Practical Next Steps
Build the cipher first, make the known-answer test pass, then write the cracker. That order makes failures easy to localise: if the cracker recovers "LEMON" from ciphertext you generated yourself, both halves work.
Reasonable extensions include implementing Kasiski's method properly and comparing its length guesses with the chi-squared search, testing how accuracy falls as the message shrinks from two thousand characters to two hundred, and using a realistic plaintext sample rather than a pangram, since a pangram has the wrong letter distribution.
One warning worth stating plainly: the Vigenère cipher is a teaching tool, not a security tool. A pencil-and-paper attack from the nineteenth century defeats it, and a modern machine does it in milliseconds. If you need to protect real data, use a vetted library implementing AES-GCM or ChaCha20-Poly1305 rather than anything written while learning. The value of this project is understanding why frequency analysis works, why repeating a key is dangerous, and what a serious cipher has to withstand.
Photo: Daniil Komov / Pexels



