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", "rambutan", "durian", "starfruit"] def pick_word(): return random.choice(WORDS) print(pick_word()) # → durian (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", "rambutan", "durian", "starfruit", "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.
Try It Yourself
13 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.
Write 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.
Wire 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.
Mini-Challenge · 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.
Homework
4 minTake your hangman.py from class and polish it.
- Expand the
WORDSlist to at least 15 words on a theme you care about — Malaysian 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", "rambutan", "durian", "starfruit", "pineapple", "papaya", "lychee", "rojak", "guava", "longan", "mangosteen", "soursop", "jambu", "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.