Learning Goals
3 minBy the end of this lesson you can:
- Initialise a
livescounter and decrement it only on wrong guesses — not on repeats and not on invalid input. - End the loop with two different
breaks: one for winning (mask revealed), one for losing (lives hit zero). - Print a different ending message for win vs lose, using the state of the loop when it exited.
Warm-Up
5 minTwo questions to think about before we write code:
- Last lesson, was a wrong guess punished? (Answer: no — the player could guess forever.) What changes today is that wrong guesses cost a life.
- When the player types a letter they've already guessed, should they lose a life? Most Hangman implementations say no. Repeats are just bounced back with a polite message.
So we need to tell three cases apart: right guess (mask changes, no life lost), wrong guess (mask doesn't change, lose one life), repeat guess (skip everything, lose no life).
Compare the mask before and after the reveal call. If it's the same, no letters were uncovered, so the guess was wrong. If it changed, the guess was right.
New Concept · Lives Counter & Win/Lose
14 minThe lives counter pattern
Same shape as the score counter from PY-L1-33 — but counting down, not up:
lives = 6 while lives > 0: # ... ask, judge ... if wrong: lives = lives - 1 print("Lives left:", lives)
The while lives > 0: in the loop header is half the win/lose logic — the moment lives hits 0, the condition becomes False and the loop exits on its own. No break needed for losing.
The before/after comparison
How do we know if the guess was right or wrong? Save the mask before calling reveal, then compare:
old_mask = mask mask = reveal(secret, mask, guess) if mask == old_mask: # nothing changed → wrong guess print("❌ Nope.") lives = lives - 1 else: # something revealed → right guess print("✅ Good guess!")
The reveal function from Part 1 doesn't need to know whether the guess was right. The main program figures it out by comparing.
Two ways out of the loop
The loop ends in one of two ways:
- Win. Inside the loop, after a successful reveal, check
if is_revealed(mask)andbreak. - Lose. The
while lives > 0:condition becomes false, the loop exits naturally — nobreak.
To tell them apart after the loop, just check what state the variables are in:
# after the loop if is_revealed(mask): print("🎉 You won!") else: print("💀 Game over. The word was", secret)
If we exited because we won, the mask is fully revealed. If we exited because we ran out of lives, it isn't. One little if tells the two endings apart.
Why 6 lives?
Six is the classic Hangman number — head, body, two arms, two legs. Any number works. 3 for very hard, 10 for very forgiving.
Don't cost a life on repeats
The continue idiom from Part 1 helps. Before we touch reveal, check if the letter is in the guessed list — if it is, scold and continue. Lives don't change.
if guess in guessed: print("Already tried that.") continue # ← jumps back to top, no life lost guessed.append(guess) # only NOW do we call reveal and possibly lose a life
Use continue for "skip this iteration politely". Use break for "the game is over." Use the loop condition for "keep going as long as this is true."
Worked Example · Hangman Lite v2
13 minThe full file
Save (or update) hangman.py:
Code
# hangman.py — Part 2: lives & win/lose import random WORDS = [ "banana", "mango", "rambutan", "durian", "starfruit", "pineapple", "papaya", "lychee", "rojak", "guava", "longan", "mangosteen", "soursop", "jambu", "pomelo", ] LIVES = 6 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 = [] lives = LIVES print("=" * 40) print(" Hangman Lite — Hub Edition") print("=" * 40) print("Word:", mask, " (length", len(secret), ")") print("You have", lives, "lives.") print() while lives > 0: print("Already guessed:", guessed, "| Lives:", lives) guess = input("Guess: ").strip().lower() if len(guess) != 1 or not guess.isalpha(): print("One letter at a time, please.") continue if guess in guessed: print("You already tried that one.") continue guessed.append(guess) old_mask = mask mask = reveal(secret, mask, guess) if mask == old_mask: lives = lives - 1 print("❌ Nope. Lives left:", lives) else: print("✅ Good guess! Word:", mask) if is_revealed(mask): break # the loop has ended — decide why print() print("=" * 40) if is_revealed(mask): print("🎉 You won! The word was", secret) else: print("💀 Game over. The word was", secret) print("=" * 40)
Sample run · a clean win
========================================
Hangman Lite — Hub Edition
========================================
Word: ------ (length 6)
You have 6 lives.
Already guessed: [] | Lives: 6
Guess: a
✅ Good guess! Word: -a-a-a
Already guessed: ['a'] | Lives: 6
Guess: n
✅ Good guess! Word: -anana
Already guessed: ['a', 'n'] | Lives: 6
Guess: b
✅ Good guess! Word: banana
========================================
🎉 You won! The word was banana
========================================Sample run · a sad loss
Guess: x ❌ Nope. Lives left: 5 Guess: q ❌ Nope. Lives left: 4 Guess: z ❌ Nope. Lives left: 3 Guess: y ❌ Nope. Lives left: 2 Guess: w ❌ Nope. Lives left: 1 Guess: v ❌ Nope. Lives left: 0 ======================================== 💀 Game over. The word was banana ========================================
Things worth pausing on
not guess.isalpha()is a new tiny method — it returnsFalseif the string contains anything other than letters. Numbers and punctuation get politely rejected with the "one letter at a time" message and no life lost. (You can drop it if you find it tricky.)LIVESat the top as a constant means changing the difficulty is a one-line edit. SetLIVES = 3for hard mode.- Same ending check inside and after the loop.
if is_revealed(mask)is used twice — once tobreakon a win, once to decide which ending message to print. The function from yesterday earned its keep.
From a list, a mask, a reveal, a counter, a loop and two breaks. Open it and play five rounds. Drop your favourite words in. Lend it to a friend. That's the whole point of writing Python.
Try It Yourself
14 minThree tasks. Build the lives counter first, then handle the win/lose ending, then add a polish touch.
Take your Part 1 file. Add lives = 6 before the loop. Inside the loop, after the reveal call, compare the mask before and after — if it didn't change, decrement lives by 1. Change the while True at the top of the loop to while lives > 0.
Hint
lives = 6 while lives > 0: guess = input("Guess: ").strip().lower() # ...validation... old_mask = mask mask = reveal(secret, mask, guess) if mask == old_mask: lives = lives - 1 print("Wrong! Lives left:", lives) else: print("Right!")
Try it. Type six wrong letters in a row. The loop should exit on its own when lives hits 0.
After the loop, write an if that checks is_revealed(mask). If true, print a win message. If false, print a lose message that reveals the secret word.
Hint
# after the loop if is_revealed(mask): print("🎉 You won!") else: print("💀 Game over. The word was", secret)
The state of the mask at exit time tells you which path you took. No need for an extra flag variable — the existing state already encodes the answer.
If you didn't build the guessed list in your Part 1 homework, add it now. Then make sure that typing an already-guessed letter doesn't cost a life — use continue to skip back to the top of the loop before any decrement happens.
Hint
# inside the loop, near the top if guess in guessed: print("Already tried that — no penalty.") continue guessed.append(guess) # the rest of the iteration runs only for new guesses
Test it. Type a ten times. The first one might count; the next nine should bounce off without changing lives. This is what makes Hangman feel fair to play.
Mini-Challenge · Tina's Unfair Hangman
8 minTina's Hangman is unwinnable — even correct guesses cost a life, and a repeat costs two. Find the three bugs.
# tina_hangman.py — buggy main loop
lives = 6
guessed = []
while lives > 0:
guess = input("Guess: ").strip().lower()
guessed.append(guess) # appended before the repeat check
if guess in guessed:
lives = lives - 1
print("Repeat!")
continue # already lost the life
old_mask = mask
mask = reveal(secret, mask, guess)
lives = lives - 1 # ALWAYS loses a life
print("Lives left:", lives)
if is_revealed(mask):
break- Bug 1.
guessed.append(guess)happens before the repeat check, soguess in guessedis always true after the first iteration. - Bug 2. Even when she correctly detects a repeat, she also drops a life — so a repeat costs 1, then the "always loses a life" further down would cost another (except
continueskips it, but the first decrement still happened). - Bug 3. The
lives = lives - 1at the bottom runs on every iteration, not just on wrong guesses. Wrap it inif mask == old_mask:so it only fires for misses.
Show one possible fix
# tina_hangman.py — fixed lives = 6 guessed = [] while lives > 0: guess = input("Guess: ").strip().lower() if guess in guessed: # repeat check FIRST print("Repeat — no penalty.") continue guessed.append(guess) old_mask = mask mask = reveal(secret, mask, guess) if mask == old_mask: # only wrong guesses cost a life lives = lives - 1 print("Wrong! Lives left:", lives) else: print("Right! Word:", mask) if is_revealed(mask): break
The order of operations matters enormously in a game loop. Check repeats first (skip if so), then add to guessed, then call reveal, then decide if a life was lost. Reorder these steps and you get a different (often unfair) game.
Recap
3 minHangman Lite is complete. A lives counter starts at 6 and ticks down only on wrong guesses — we detect "wrong" by comparing the mask before and after reveal. The while lives > 0 loop condition does the "ran out of lives" case automatically, and a break handles the "won" case. After the loop, one if is_revealed(mask) tells the two endings apart. Repeats are handled with continue so they cost nothing. The functions from Part 1 — pick_word, make_mask, reveal, is_revealed — didn't change a line. That's the function payoff at its purest: yesterday's reliable code, plugged into today's richer game. Next lesson we polish the look — ASCII gallows that draw step by step.
Vocabulary Card
- lives counter
- A variable that starts high and is decremented on failure. The loop ends when it reaches zero.
- before/after compare
- Saving a value before calling a function and comparing afterwards, to detect whether anything changed.
- two-exit loop
- A loop that can end either by its condition becoming false (e.g. lives ran out) or by a
break(e.g. won). str.isalpha()- A string method that returns
Trueonly if every character is a letter. Useful for validating one-letter input. - fairness
- The design choice that "the punishment should match the mistake". Repeats and bad input shouldn't cost a life.
Homework
4 minPolish hangman.py and bring the finished playable game next class.
- Finish all three tasks from Try It Yourself if you haven't already — lives, win/lose ending, repeat protection.
- Add a constant
LIVES = 6at the top of your file. Use it where you currently have a literal6. (Future-you will change the difficulty in one place.) - Add a final tally line after the ending:
You used <len(guessed)> guesses.Useful and free —guessedalready tracks every unique letter tried. - Play three rounds. Note in a comment at the top of the file: did you win or lose each round? It'll feel like a real piece of software.
- Bonus: wrap the whole game in
while True:with a "play again? (y/n)" prompt at the end, so you can play round after round without restarting the script. (Hint: resetsecret,mask,guessedandlivesinside the outer loop.)
Bring hangman.py next class — in PY-L1-37 we add the ASCII gallows.
Sample · hangman.py (with replay)
# hangman.py — finished playable Hangman Lite import random WORDS = [ "banana", "mango", "rambutan", "durian", "starfruit", "pineapple", "papaya", "lychee", "rojak", "guava", "longan", "mangosteen", "soursop", "jambu", "pomelo", ] LIVES = 6 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 # outer loop — play again? while True: secret = pick_word() mask = make_mask(secret) guessed = [] lives = LIVES print("=" * 40) print(" Hangman Lite") print("=" * 40) print("Word:", mask, " (length", len(secret), ")") print("You have", lives, "lives.") while lives > 0: print() print("Guessed:", guessed, "| Lives:", lives) guess = input("Guess: ").strip().lower() if len(guess) != 1 or not guess.isalpha(): print("One letter at a time, please.") continue if guess in guessed: print("Already tried — no penalty.") continue guessed.append(guess) old_mask = mask mask = reveal(secret, mask, guess) if mask == old_mask: lives = lives - 1 print("❌ Nope. Lives left:", lives) else: print("✅ Right! Word:", mask) if is_revealed(mask): break print() print("=" * 40) if is_revealed(mask): print("🎉 You won! The word was", secret) else: print("💀 Game over. The word was", secret) print("You used", len(guessed), "guesses.") print("=" * 40) again = input("Play again? (y/n) ").strip().lower() if again != "y": print("Thanks for playing!") break
The outer while True loop is the "play again" pattern from PY-L1-20's Magic 8-Ball — same shape, reused. Notice every variable that needs to reset (secret, mask, guessed, lives) is assigned inside the outer loop, so each new round starts clean. That's the kind of small detail you'll learn to spot automatically by the end of Level 2.