Learning Goals
3 minBy the end of this lesson you can:
- Pick a random word from a list with
random.choice()(callback from PY-L1-19) inside a function. - Build a "mask" for the secret word — a string of dashes the same length as the word.
- Update the mask one letter at a time as the player guesses, by walking the secret word with a
forloop and an index.
Warm-Up
5 minQuick recall — how would you hide a word behind dashes? If the word is banana (six letters), the mask is ------. Six dashes, one per letter. Python lets you build that mask in one line:
word = "banana" mask = "-" * len(word) print(mask) # → ------
The * trick on strings comes from PY-L1-05 — multiply a string and you get it repeated. Combined with len(), you get a mask that's always the right length, however long the word is.
Now the harder predict-the-output. If the player guesses a and the word is banana, what should the mask become?
Show the answer
-a-a-a
Every a in banana sits at positions 1, 3 and 5 (zero-indexed). They all reveal at once. Today we work out how to compute that.
Strings in Python are immutable — you can't change one letter inside an existing string. You can't write mask[1] = "a"; Python will throw a TypeError. We'll build a new mask each time instead.
New Concept · Reveal-on-Guess
14 minPick a secret word
From PY-L1-19, random.choice(list) picks a random item:
import random WORDS = ["banana", "mango", "plum", "melon", "pear"] def pick_word(): return random.choice(WORDS) print(pick_word()) # → melon (or any of the five, randomly)
One-line function. The WORDS list lives at the top of the file in capitals — that's a convention to say "this is a fixed configuration value, treat it as a constant".
Make the initial mask
def make_mask(word): return "-" * len(word) secret = "banana" print(make_mask(secret)) # → ------
Six characters of word → six dashes. The function works for words of any length, which matters because we don't know which word pick_word will hand us.
Reveal a guessed letter — the key trick
This is the lesson's real idea. Walk the secret word with a for loop. For each position, look at two things — the letter in the secret, and the letter in the current mask. If the secret letter matches the guess, take it; otherwise, keep what was in the mask.
def reveal(secret, mask, guess): new_mask = "" for i in range(len(secret)): if secret[i] == guess: new_mask = new_mask + secret[i] # show the real letter else: new_mask = new_mask + mask[i] # keep whatever was already there return new_mask
Read it carefully. We build new_mask from scratch every time, one character at a time. At position i:
- If
secret[i]matches the guess, add the real letter. - Otherwise, copy across whatever was in the existing
maskat positioni— a dash if it's still hidden, or a letter that was revealed by an earlier guess.
See it in action
secret = "banana" mask = "------" mask = reveal(secret, mask, "a") print(mask) # → -a-a-a mask = reveal(secret, mask, "n") print(mask) # → -anana mask = reveal(secret, mask, "b") print(mask) # → banana
Three calls, three reveals. Each one builds a fresh mask using the secret + the previous mask + the new guess.
Why we don't mutate the mask in place
mask = "------" mask[1] = "a" # TypeError: 'str' object does not support item assignment
Strings can't be changed letter-by-letter (that's what immutable means). The work-around — building a new string char-by-char with new_mask + char — is the Level-1 idiom you'll use any time you need to "edit" a string.
To change a string, build a new string. Loop through the original, pick the character you want at each position, and concatenate them into a fresh variable.
Worked Example · Hangman Lite v1
14 minThe full file
Save as hangman.py:
Code
# hangman.py — Part 1: pick & reveal import random WORDS = ["banana", "mango", "plum", "melon", "pear", "pineapple", "papaya", "lychee", "rojak"] def pick_word(): return random.choice(WORDS) def make_mask(word): return "-" * len(word) def reveal(secret, mask, guess): new_mask = "" for i in range(len(secret)): if secret[i] == guess: new_mask = new_mask + secret[i] else: new_mask = new_mask + mask[i] return new_mask # main program — Part 1 (no lives yet) secret = pick_word() mask = make_mask(secret) print("Welcome to Hangman Lite!") print("Word:", mask, " (length", len(secret), ")") print("Type one letter at a time. Type 'quit' to stop.") print("(Part 2 adds lives — for now you can guess forever.)") while True: guess = input("Guess: ").strip().lower() if guess == "quit": break if len(guess) != 1: print("One letter at a time, please.") continue mask = reveal(secret, mask, guess) print("Word:", mask) if mask == secret: print("🎉 You revealed it! The word was", secret) break
Sample run
Welcome to Hangman Lite! Word: ------ (length 6) Type one letter at a time. Type 'quit' to stop. (Part 2 adds lives — for now you can guess forever.) Guess: a Word: -a-a-a Guess: n Word: -anana Guess: z Word: -anana Guess: b Word: banana 🎉 You revealed it! The word was banana
Three things to spot
continuefor invalid input. If the player types more than one letter, we print a polite message and skip the rest of the loop —continuejumps straight back to the top.mask == secretis the "all revealed" check. The moment the mask matches the secret exactly, every letter has been revealed — we win andbreak.- Wrong guesses still update the mask.
revealdoesn't care if the guess matches or not — it just builds a new mask. If no letter matches, the new mask is identical to the old one. That's harmless.
Next lesson we add a lives counter that goes down on wrong guesses, a guessed list so the same letter doesn't cost two lives, and a proper win/lose ending. The functions we wrote today will not change.
Basic
5 minThree tasks. Build each function on its own first, then bolt them into the game.
Write make_mask(word) that returns a string of dashes the same length as word. Test it on "banana" (expect ------), "rojak" (expect -----) and "a" (expect -).
Hint
def make_mask(word): return "-" * len(word) print(make_mask("banana")) # ------ print(make_mask("rojak")) # ----- print(make_mask("a")) # -
One line of body. The * on a string repeats it; len() gives the right count. Works for words of any length — including one-letter words.
Challenge 1
4 minWrite reveal(secret, mask, guess) exactly as in the worked example. Test it on:
reveal("banana", "------", "a") # should return "-a-a-a" reveal("banana", "-a-a-a", "n") # should return "-anana" reveal("banana", "-anana", "b") # should return "banana" reveal("banana", "------", "z") # should return "------" (no change)
Hint
def reveal(secret, mask, guess): new_mask = "" for i in range(len(secret)): if secret[i] == guess: new_mask = new_mask + secret[i] else: new_mask = new_mask + mask[i] return new_mask print(reveal("banana", "------", "a")) # -a-a-a print(reveal("banana", "-a-a-a", "n")) # -anana print(reveal("banana", "-anana", "b")) # banana print(reveal("banana", "------", "z")) # ------
The key insight is else: new_mask = new_mask + mask[i] — when the guess doesn't match, copy across whatever the mask was already showing. Don't put a dash; that would erase earlier reveals.
Challenge 2
4 minWire the two functions into a tiny game. Hard-code secret = "banana" for now. Print the initial mask. In a while True loop, ask for a guess, call reveal, print the new mask, and break when mask == secret.
Hint
secret = "banana" mask = make_mask(secret) print("Word:", mask) while True: guess = input("Guess one letter: ").strip().lower() mask = reveal(secret, mask, guess) print("Word:", mask) if mask == secret: print("Won!") break
The whole game in eight lines. Type letters one at a time and watch the dashes turn into letters. This is already Hangman — without the lives, which we add in Part 2.
Challenge 3 · Khoo's Empty Mask
8 minKhoo's reveal function returns a blank string no matter what. Find the three bugs.
# khoo_reveal.py — buggy
def reveal(secret, mask, guess):
new_mask = ""
for i in range(len(secret)):
if secret[i] = guess:
new_mask = secret[i]
else:
new_mask = mask[i]
return new_mask
print(reveal("banana", "------", "a")) # should be -a-a-a- Bug 1.
secret[i] = guessuses assignment=where comparison==is needed. Also, you can't assign tosecret[i]— strings are immutable. Two bugs in one line. - Bug 2. Inside the loop,
new_mask = ...uses=instead of=with+. Every iteration overwritesnew_mask, so by the end it's just the last character. - Bug 3. The whole "build a fresh string by concatenation" idiom needs to use
new_mask = new_mask + char(ornew_mask += char) — that's the "set vs add" trap from PY-L1-31.
Show one possible fix
# khoo_reveal.py — fixed def reveal(secret, mask, guess): new_mask = "" for i in range(len(secret)): if secret[i] == guess: new_mask = new_mask + secret[i] else: new_mask = new_mask + mask[i] return new_mask print(reveal("banana", "------", "a")) # -a-a-a
All three bugs come from the same family — the difference between "set this variable" (=) and "compare these two values" (==) and "add to this variable" (= with +). Make a mental list — every time you write = inside a loop, ask: do I mean "overwrite" or "append to what's there"?
Recap
3 minHangman Lite is a three-function game. pick_word uses random.choice on a list of words. make_mask turns a word into a string of dashes the same length. reveal takes the secret, the current mask, and the player's guess, and builds a fresh new mask by walking the word one position at a time — copying the real letter where it matches, and the old mask everywhere else. Because Python strings are immutable, we can't edit the mask in place — we always build a new one. The whole game today is just a while loop that asks for a guess, calls reveal, and stops when the mask equals the secret. Next lesson we add lives, a list of already-guessed letters, and the win/lose ending.
Vocabulary Card
- mask
- A string the same length as the secret, made of dashes for hidden letters and real letters where they've been guessed.
- secret
- The hidden word the player is trying to guess. Picked once with
random.choice. - immutable
- Can't be changed in place. Strings, numbers and tuples in Python are immutable; lists are not.
- string concatenation
- Joining strings with
+. The classic way to "edit" a string in Python: build a new one piece by piece. continue- Skip the rest of this loop iteration and jump back to the top. Useful for filtering bad input mid-loop.
Extra Mission
4 minTake your hangman.py from class and polish it.
- Expand the
WORDSlist to at least 15 words on a theme you care about — fruits, K-pop groups, premier league clubs, planets, dinosaurs. - Add a function
is_revealed(mask)that returnsTrueifmaskcontains no-characters. Use it in the main loop instead ofmask == secret— same effect, but more readable. - Add a friendly "already guessed: <none yet>" line above each prompt that prints the letters the player has tried so far. Use a list (your call: a
guessedlist that youappendto inside the loop, callback to PY-L1-17). - Notice you can also
continueif the guess is already in the list — politely tell them "already guessed!" and skip.
Bring hangman.py next class — in PY-L1-36 we add lives and the win/lose finish, completing the playable game.
Sample · hangman.py
# hangman.py — Part 1 with guessed list import random WORDS = [ "banana", "mango", "plum", "melon", "pear", "pineapple", "papaya", "lychee", "rojak", "guava", "longan", "peach", "soursop", "apple", "pomelo", ] def pick_word(): return random.choice(WORDS) def make_mask(word): return "-" * len(word) def reveal(secret, mask, guess): new_mask = "" for i in range(len(secret)): if secret[i] == guess: new_mask = new_mask + secret[i] else: new_mask = new_mask + mask[i] return new_mask def is_revealed(mask): return "-" not in mask # main program secret = pick_word() mask = make_mask(secret) guessed = [] print("Welcome to Hangman Lite!") print("Word:", mask, " (length", len(secret), ")") while True: print("Already guessed:", guessed) guess = input("Guess: ").strip().lower() if guess == "quit": break if len(guess) != 1: print("One letter at a time, please.") continue if guess in guessed: print("You already tried that one!") continue guessed.append(guess) mask = reveal(secret, mask, guess) print("Word:", mask) if is_revealed(mask): print("🎉 You revealed it! The word was", secret) break
Three small features and the game already feels real. is_revealed is a textbook boolean helper — easier to read than mask == secret. The guessed list uses in (from PY-L1-15) to check for repeats without writing any loops yourself — Python does it for you. And every continue is a polite "please try again" without breaking the loop.