Day 008 of the 100 Days of Python challenge creates a Caesar Cipher encoder and decoder. Each letter is shifted through the alphabet by a user-selected amount, turning a readable message into scrambled text — and back again.

This project is a compact introduction to two ideas that show up everywhere in programming: reusable functions that take parameters, and modular arithmetic for wrapping a value back into a valid range. It also happens to be a great first taste of how basic encryption algorithms actually work under the hood.

This guide walks through the complete Day 008 source code and explains exactly how the shifting and wraparound logic works, including one subtlety around negative numbers that trips up a lot of beginners coming from other languages.

Project Overview

The core function accepts the original text, a shift amount, and whether the user wants to encode or decode. Decoding reuses the exact same shifting logic as encoding, just by reversing the shift’s direction — multiplying it by -1 — rather than writing separate code for each direction.

Source Code

The Day 008 project is contained in day008/main.py in the 100daysofpython repository, and imports its title art from a separate art.py file in the same folder:

from art import LOGO

print(LOGO)

alphabet = [
    'a','b','c','d','e','f','g','h','i','j','k','l','m',
    'n','o','p','q','r','s','t','u','v','w','x','y','z'
]

def plain2cipher(original_text, shift_amount, encode_or_decoder):
    output_text = ""
    selected_encode_or_decoder = ""

    if encode_or_decoder == "2":
        selected_encode_or_decoder = "decode"
        shift_amount *= -1
    else:
        selected_encode_or_decoder = "encode"

    for letter in original_text:
        if letter not in alphabet:
            output_text += letter
        else:
            shifted_position = alphabet.index(letter) + shift_amount
            shifted_position %= len(alphabet)
            output_text += alphabet[shifted_position]

    print("****************************************************************")
    print(f"Here is your secret message {selected_encode_or_decoder}d result: {output_text}")
    print("****************************************************************")

def caesar_cipher():
    should_continue = True

    while should_continue:
        while True:
            direction = input("Type '1' to encode, type '2' to decode: ")
            if direction == "1" or direction == "2":
                break
            print("Invalid input. Please type '1' to encode or '2' to decode.")
            print("****************************************************************")

        text = input("Type your message: ").lower()
        shift = int(input("Type the shift number: "))

        plain2cipher(original_text=text, shift_amount=shift, encode_or_decoder=direction)

        restart = input("Type 'y' if you want to go again. Otherwise, type 'n': ").lower()

        if restart == "n":
            should_continue = False
            print("Goodbye")

caesar_cipher()

How the Program Works

One Function Handles Both Directions

if encode_or_decoder == "2":
    selected_encode_or_decoder = "decode"
    shift_amount *= -1
else:
    selected_encode_or_decoder = "encode"

Rather than writing separate encode and decode functions, plain2cipher() reuses the same shifting loop for both. Decoding is really just encoding with the shift reversed — if encoding shifts every letter forward by 3, decoding shifts every letter backward by 3, which is exactly what shift_amount *= -1 accomplishes.

Finding and Shifting a Letter’s Position

shifted_position = alphabet.index(letter) + shift_amount
shifted_position %= len(alphabet)
output_text += alphabet[shifted_position]

alphabet.index(letter) finds a letter’s numeric position ('a' is 0, 'b' is 1, and so on). Adding shift_amount moves that position forward or backward, and %= len(alphabet) wraps the result back into a valid index — so shifting 'z' forward by 1 wraps around to 'a' instead of producing an invalid index.

Why Modulo Works Correctly Even With Negative Shifts

This is the subtle part. When decoding, shift_amount is negative, which means shifted_position can also end up negative — for example, alphabet.index('a') + (-3) is -3. In some languages, taking the modulo of a negative number returns a negative result, which would crash alphabet[shifted_position] with an invalid index. In Python, % always returns a result with the same sign as the divisor, so -3 % 26 correctly evaluates to 23, not -3. That behavior is exactly why this code works correctly for decoding without any extra sign-handling logic — it’s relying on a genuine, useful quirk of how Python’s modulo operator is defined.

Preserving Non-Letter Characters

if letter not in alphabet:
    output_text += letter

Since alphabet only contains lowercase letters, any character that isn’t in it — spaces, punctuation, numbers — is added to the output unchanged. Combined with text = input("Type your message: ").lower() earlier, this means the cipher works correctly on full sentences, not just single words.

What This Project Teaches

Day 008 introduces:

  • Function parameters and returning to the same function for two different behaviors
  • List indexing with .index()
  • Modular arithmetic (%) for wrapping values into a valid range
  • How Python’s modulo operator behaves differently from some other languages with negative numbers
  • Preserving non-alphabet characters during text processing

A Caesar Cipher is educational rather than secure encryption, because its small key space (only 26 possible shifts) can be guessed by brute force almost instantly. The project is best understood as an introduction to algorithms and string processing, not real-world cryptography.

Running the Project

git clone https://github.com/victorhugo81/100daysofpython.git
cd 100daysofpython/day008
python main.py

The project also imports its logo from the folder’s art.py file, which must stay in the same directory as main.py.

Possible Next Steps

Once you understand the existing code, try extending it:

  • Wrap the shift input in a try/except block — unlike the direction prompt, int(input(...)) here has no validation, so typing a non-numeric shift crashes the program
  • Support uppercase letters directly, instead of relying on .lower() to force everything to lowercase first
  • Try every possible shift (0–25) automatically and print all the results, to demonstrate how easily a Caesar Cipher can be brute-forced
  • Add a simple word-list check to automatically flag which decoded result looks like real English text

Conclusion

Day 008 of the 100 Days of Python challenge turns a simple mathematical idea — shifting letters and wrapping around the end of the alphabet — into a working encoder and decoder using functions, list indexing, and modular arithmetic. Understanding exactly why Python’s % operator handles negative shifts correctly is a small detail, but it’s the kind of language-specific behavior that’s worth knowing well before you rely on it in more advanced projects.

View the Day 008 source code on GitHub, then continue through the repository’s project index as you build more Python programs, one day and one small project at a time.