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
nNotice
Positions start at 0, so word[0] is the FIRST character.
word = "nvvg"
print(word[1])
print(word[2])
Output
v
vNotice
Each number picks the character sitting at that position.
word = "nvvg"
print(word[-1])
Output
gNotice
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
eNotice
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
vvgNotice
[1:4] takes positions 1, 2, 3 — up to but NOT including 4.
word = "nvvg"
print(word[:2])
Output
nvNotice
Leave the start blank to begin at position 0.
word = "nvvg"
print(word[2:])
Output
vgNotice
Leave the stop blank to run all the way to the end.
word = "noon ta teem"
print(word[::-1])
Output
meet at noonNotice
[::-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 MENotice
.lower() returns a lowercase copy; note itself is unchanged.
quiet = "meet me"
print(quiet.upper())
Output
MEET MENotice
.upper() returns an uppercase copy.
padded = " meet me "
print(padded.strip())
Output
meet meNotice
.strip() removes the spaces from both ends.
coded = "meet_me_at_noon"
print(coded.replace("_", " "))
Output
meet me at noonNotice
.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 noonNotice
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 libraryThe 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
TrueNotice
in reports True/False — is "m" somewhere in letters? Yes.
letters = "abcdefghijklmnopqrstuvwxyz"
print(" " in letters)
print("!" in letters)
Output
False
FalseNotice
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 markNotice
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
TrueNotice
in tests ONE character against the vowels — 'a' is a vowel.
word = "cat"
for ch in word:
print(ch in "aeiou")
Output
False
True
FalseNotice
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
2Notice
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
1Notice
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
1Notice
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
-1Notice
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
HNotice
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
HINotice
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
HINotice
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]includesstartand stops just beforestop;[::-1]reverses.- A string method returns a changed copy, and the original stays the same.
inasks whether one value appears inside another.(position + shift) % 26wraps pastz, and decoding shifts by26 - shift.
You can now write a secret code a friend can crack.