Python by Projects

unit

Secret Codes

A folded note lands on your desk:

nvvg nv zg gsv oryizib

It looks like nonsense, but your friend promises it hides a real message. Today we crack it by taking text apart, flipping it, and rebuilding it one character at a time.

Each tool comes as a short ladder — simplest first, then one step up, then the real cipher — with a Notice naming the one new thing each time.

You will learn

  • Pick one character by its position, and a run of characters with a slice.
  • Clean text with .lower(), .upper(), .strip() and .replace().
  • Test whether a character appears in a string with in.
  • Build a new string in a loop to encode and decode a secret message.

Lesson 1: Take the Note Apart

A string is text stored in order. Each character has a numbered position, starting at 0. Square brackets select one character by its position.

word = "nvvg"
print(word[0])

Output

n

Notice

Positions start at 0, so word[0] is the FIRST character.

word = "nvvg"
print(word[1])
print(word[2])

Output

v
v

Notice

Each number picks the character sitting at that position.

word = "nvvg"
print(word[-1])

Output

g

Notice

A negative position counts back from the end — [-1] is the last character.

word = "code"
for position in range(4):
    print(word[position])

Output

c
o
d
e

Notice

A loop can walk every position in order, one character at a time.

A slice selects a RUN of characters. In text[start:stop] Python includes start and stops just before stop.

word = "nvvg"
print(word[1:4])

Output

vvg

Notice

[1:4] takes positions 1, 2, 3 — up to but NOT including 4.

word = "nvvg"
print(word[:2])

Output

nv

Notice

Leave the start blank to begin at position 0.

word = "nvvg"
print(word[2:])

Output

vg

Notice

Leave the stop blank to run all the way to the end.

word = "noon ta teem"
print(word[::-1])

Output

meet at noon

Notice

[::-1] steps backward, reversing the whole string — a quick reverse cipher.

Messages often arrive with extra spaces, mixed case, or separator marks. String methods make a CHANGED copy (the original stays the same). This unit uses four: .lower(), .upper(), .strip(), .replace(old, new).

note = "MEET ME"
print(note.lower())
print(note)

Output

meet me
MEET ME

Notice

.lower() returns a lowercase copy; note itself is unchanged.

quiet = "meet me"
print(quiet.upper())

Output

MEET ME

Notice

.upper() returns an uppercase copy.

padded = "   meet me   "
print(padded.strip())

Output

meet me

Notice

.strip() removes the spaces from both ends.

coded = "meet_me_at_noon"
print(coded.replace("_", " "))

Output

meet me at noon

Notice

.replace(old, new) swaps every old piece for new.

messy = "   MEET_ME_AT_NOON   "
clean = messy.strip().lower().replace("_", " ")
print(clean)

Output

meet me at noon

Notice

Methods can be CHAINED — each one works on the result of the one before it.

Lesson 2: Decode the Folded Note

Our note uses a swap: the first alphabet letter trades with the last, the second with the second-to-last, and so on. The loop checks all 26 positions to find each coded letter; a letter at position has its partner at 25 - position. Characters not found (a space or mark) are copied unchanged.

letters = "abcdefghijklmnopqrstuvwxyz"
coded_note = "nvvg nv zg gsv oryizib"
coded_note = coded_note.lower()
decoded_note = ""

for letter in coded_note:
    new_letter = letter
    for position in range(26):
        if letters[position] == letter:
            new_letter = letters[25 - position]
    decoded_note = decoded_note + new_letter

print(decoded_note)

Output

meet me at the library

The note says meet me at the library. Trace one coded character through the inner loop: which position is found, and which partner is used? Why does the accumulator begin as an empty string?

Try it yourself

coded_note = input("Type the coded note: ")
letters = "abcdefghijklmnopqrstuvwxyz"
coded_note = coded_note.lower()
decoded_note = ""

for letter in coded_note:
    new_letter = letter
    for position in range(26):
        if letters[position] == letter:
            new_letter = letters[25 - position]
    decoded_note = decoded_note + new_letter

print(decoded_note)

Notice

(It reads live input, so it does not run here.)

To WRITE a cipher we must tell letters from marks. The in operator asks whether one value appears inside another.

letters = "abcdefghijklmnopqrstuvwxyz"
print("m" in letters)

Output

True

Notice

in reports True/False — is "m" somewhere in letters? Yes.

letters = "abcdefghijklmnopqrstuvwxyz"
print(" " in letters)
print("!" in letters)

Output

False
False

Notice

A space and a ! are not letters, so in is False for them.

letters = "abcdefghijklmnopqrstuvwxyz"
message = "hi!"
for character in message:
    if character in letters:
        print(character + " is a letter")
    else:
        print(character + " is a mark")

Output

h is a letter
i is a letter
! is a mark

Notice

Inside a loop, in tests each character — here it just labels them; the real cipher will use that test to shift letters and copy marks unchanged.

Lesson 3: Write a Code Your Friend Can Crack

Now we shift each letter forward, leaving spaces and punctuation untouched. Here is a deliberate bug: a late letter can step past the end of the alphabet. Predict which line the traceback points to when the message starts with z.

Read the error

def encode_without_wrap(message, shift):
    letters = "abcdefghijklmnopqrstuvwxyz"
    message = message.lower()
    result = ""
    for letter in message:
        if letter in letters:
            for position in range(26):
                if letters[position] == letter:
                    new_position = position + shift
                    result = result + letters[new_position]
        else:
            result = result + letter
    return result


print(encode_without_wrap("zoo", 3))

Read a traceback from the bottom: IndexError means a position fell outside the string. The remainder operator repairs the wrap — (position + shift) % 26 is always 0 through 25.

def encode(message, shift):
    letters = "abcdefghijklmnopqrstuvwxyz"
    message = message.lower()
    result = ""
    for letter in message:
        if letter in letters:
            for position in range(26):
                if letters[position] == letter:
                    new_position = (position + shift) % 26
                    result = result + letters[new_position]
        else:
            result = result + letter
    return result


coded_message = encode("Meet me at 4!", 3)
print(coded_message)

Output

phhw ph dw 4!

Trace the first m: the scan finds position 12, adds shift 3, selects position 15 — p. For z, 25 + 3 = 28, and 28 % 26 wraps to 2 — c. The result variable is a string accumulator, built left to right.

def decode(message, shift):
    return encode(message, 26 - shift)


original_message = "meet me at 4!"
coded_message = encode(original_message, 3)
decoded_message = decode(coded_message, 3)
print(f"Coded: {coded_message}")
print(f"Decoded: {decoded_message}")

Output

Coded: phhw ph dw 4!
Decoded: meet me at 4!

Encoding then decoding is a round trip. Because the encoder lowercases first, the decoded text matches the lowercase original; spaces, the number, and ! survive unchanged.

Try it yourself

message = input("Type a secret message: ")
shift = 3
coded_message = encode(message, shift)
print(coded_message)

Notice

(It reads live input, so it does not run here.)

Trade coded messages with a partner and share the shift separately. Predict how spaces and punctuation will look, then explain why both friends need the same shift.

Algorithm Extension

The algorithm-pattern Spotlights for this unit, gathered here for reference — read them alongside the Algorithm Extension exercises.

Pattern Spotlight: Scan until found + stop early

Kid hook: check items one by one; break when found = stop working early. A person may spot a letter at a glance, but the program checks positions in order. After the comparison finds the letter, break avoids checking the rest. (The scan in the lesson above checks all 26 positions; Exercise 14 adds the break so it can stop early.)

print("a" in "aeiou")

Output

True

Notice

in tests ONE character against the vowels — 'a' is a vowel.

word = "cat"
for ch in word:
    print(ch in "aeiou")

Output

False
True
False

Notice

Run the same in test on each character in turn — cat gives False, True, False.

word = "cat"
position = 0
for ch in word:
    print(position)
    position = position + 1

Output

0
1
2

Notice

A position counter, stepped once per character, tracks WHERE we are: 0, 1, 2.

word = "cat"
position = 0
found = -1
for ch in word:
    if ch in "aeiou":
        found = position
    position = position + 1
print(found)

Output

1

Notice

Instead of printing every step, remember the index of the vowel in found (a -1 stand-in for "none yet") — the vowel of cat is at index 1. Without a break this keeps the LATEST match; R5's break will keep the FIRST.

word = "cat"
position = 0
found = -1
for ch in word:
    if ch in "aeiou":
        found = position
        break
    position = position + 1
print(found)

Output

1

Notice

break STOPS at the FIRST vowel instead of scanning the rest — same index 1, less work.

word = "myth"
position = 0
found = -1
for ch in word:
    if ch in "aeiou":
        found = position
        break
    position = position + 1
print(found)

Output

-1

Notice

myth has no vowel, so the loop falls through and the -1 stand-in reports "not found".

Pattern Spotlight: Do the same to each (map)

To apply the same step to every item, producing a new sequence, loop over the message one character at a time and add each transformed character to a new string. A whole-string method changes the message in one call; an explicit per-character loop builds a transformed result for every character, unlike counting only characters that meet a condition.

result = ""
result = result + "h".upper()
print(result)

Output

H

Notice

Transform one character with .upper() and add it to the new string — "h" becomes "H".

result = ""
result = result + "h".upper()
result = result + "i".upper()
print(result)

Output

HI

Notice

The same step applied again grows the transformed string to "HI".

word = "hi"
result = ""
for ch in word:
    result = result + ch.upper()
print(result)

Output

HI

Notice

The loop applies the same .upper() transform to EVERY character of "hi", building "HI".

Recap

  • Positions start at 0, and word[-1] is the last character.
  • text[start:stop] includes start and stops just before stop; [::-1] reverses.
  • A string method returns a changed copy, and the original stays the same.
  • in asks whether one value appears inside another.
  • (position + shift) % 26 wraps past z, and decoding shifts by 26 - shift.

You can now write a secret code a friend can crack.

Go to the exercises (24)

For parents and teachers: report a problem