Learning Goals
3 minBy the end of this lesson you can:
- Store a multi-line string with triple quotes (
"""...""") and print it intact. - Put seven multi-line pictures in a list and index in with
GALLOWS[lives]to pick the right one. - Wrap the picture-printing in a function
draw_gallows(lives)so the main loop stays readable.
Warm-Up
5 minYou already know that "\n" inside a string makes a newline. Python has a nicer way for long strings — triple quotes. Anything between them is preserved exactly, line breaks and all:
art = """ +---+ | | | | | | =========""" print(art)
+---+
| |
|
|
|
|
=========That's the empty gallows — no stick figure yet. One print(), six lines of output. No string concatenation acrobatics.
"""...""" is the same string type as "...". You can store it in a variable, put it in a list, pass it to a function. The only thing special is that newlines inside are kept as-is.
New Concept · A List of Pictures
14 minSeven pictures, one per state
We have 7 possible "lives" values — 6, 5, 4, 3, 2, 1, 0. So we need 7 pictures: empty gallows, head, head+body, +one arm, +two arms, +one leg, +two legs (dead).
Store them in a list — the index IS the lives count
Clever bit: put them in a list where index 0 is the full hangman (0 lives left = dead) and index 6 is the empty gallows (6 lives = nothing drawn yet). Then GALLOWS[lives] gives the right picture automatically:
GALLOWS = [ # index 0 — dead (no lives left) """ +---+ | | O | /|\\ | / \\ | | =========""", # index 1 """ +---+ | | O | /|\\ | / | | =========""", # index 2 """ +---+ | | O | /|\\ | | | =========""", # index 3 """ +---+ | | O | /| | | | =========""", # index 4 """ +---+ | | O | | | | | =========""", # index 5 """ +---+ | | O | | | | =========""", # index 6 — empty gallows (full lives) """ +---+ | | | | | | =========""", ]
One function does the picking
def draw_gallows(lives): print(GALLOWS[lives])
Two lines. GALLOWS[lives] reads "the picture at position lives". When lives is 6, we get the empty gallows. When lives drops to 5, we get the head-only picture. When lives hits 0, we get the full hangman.
Why the index ordering matters
The reason index 0 = full hangman and index 6 = empty gallows is so the index directly equals the lives count. No subtraction, no len(GALLOWS) - 1 - lives arithmetic. The list's shape is doing the work.
Why backslashes need escaping
Look at the arms — written as /|\\ in the strings above. To get a single backslash into a Python string, you type \\\\ (two backslashes). One backslash on its own would start an escape sequence (\\n, \\t, etc.). Two backslashes mean "literal backslash". This is the only fiddly bit about ASCII art in Python.
Whenever you have several pictures (or messages) corresponding to a number, put them in a list and use the number as the index. The list ordering itself becomes the lookup table.
Worked Example · Hangman Lite v3
14 minBolt the gallows onto Part 2
Update hangman.py. The functions and main loop from Part 2 don't change; we just add GALLOWS + draw_gallows and call the function after each wrong guess and once at the start.
Code
# hangman.py — Part 3: ASCII gallows import random WORDS = [ "banana", "mango", "rambutan", "durian", "starfruit", "pineapple", "papaya", "lychee", "rojak", "guava", ] LIVES = 6 GALLOWS = [ """ +---+ | | O | /|\\ | / \\ | | =========""", """ +---+ | | O | /|\\ | / | | =========""", """ +---+ | | O | /|\\ | | | =========""", """ +---+ | | O | /| | | | =========""", """ +---+ | | O | | | | | =========""", """ +---+ | | O | | | | =========""", """ +---+ | | | | | | =========""", ] 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 def draw_gallows(lives): print(GALLOWS[lives]) # main program secret = pick_word() mask = make_mask(secret) guessed = [] lives = LIVES print("=" * 40) print(" Hangman Lite v3") print("=" * 40) draw_gallows(lives) print("Word:", mask, " (length", len(secret), ")") 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 draw_gallows(lives) 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("=" * 40)
Sample run · a slow loss
========================================
Hangman Lite v3
========================================
+---+
| |
|
|
|
|
=========
Word: ------ (length 6)
Guessed: [] | Lives: 6
Guess: x
+---+
| |
O |
|
|
|
=========
❌ Nope. Lives left: 5
Guessed: ['x'] | Lives: 5
Guess: y
+---+
| |
O |
| |
|
|
=========
❌ Nope. Lives left: 4
... (you get the idea — every wrong guess adds a body part)Two important details
- Draw the gallows once before the loop too. The first call to
draw_gallows(lives)happens up there withlives=6so the player sees the empty frame before they've guessed anything. - Only draw after wrong guesses. A right guess doesn't change the picture, so don't print it — fewer screen-fulls of repetitive art.
The mechanics didn't change from Part 2 — what changed is the feedback. Watching a stick figure assemble itself is a dramatically different experience from reading "Lives left: 3". The lesson here isn't just ASCII — it's that visual feedback transforms how a game feels, even when the code underneath is identical.
Try It Yourself
13 minThree tasks. Start small, then bolt it in.
Open a fresh file ascii_test.py. Make a variable frame that holds the empty gallows using triple quotes. Print it. Run it. You should see the gallows on screen with no extra blank lines.
Hint
frame = """ +---+ | | | | | | =========""" print(frame)
If you see an unwanted blank line at the start, you probably have a newline right after the opening """. Either start your art on the same line as the opening triple quote (as above), or use print(frame.strip("\\n")) to trim leading/trailing newlines.
Make a list pics with just two pictures — empty gallows at index 1, full hangman at index 0. Write a function show(n) that prints pics[n]. Call show(1), then show(0) — you've made a two-frame animation.
Hint
pics = [ """ +---+ | | O | /|\\ | / \\ | | =========""", """ +---+ | | | | | | =========""", ] def show(n): print(pics[n]) show(1) # empty gallows print("...one wrong guess later...") show(0) # dead
You've just made the simplest possible "lives left" system — one picture per state, indexed straight by the state value.
Add five more pictures to pics so you have all seven Hangman states. Write a tiny loop that calls show(n) for n from 6 down to 0, with a print("---") divider between each. You should see the stick figure assemble itself from nothing to fully drawn.
Hint
for n in range(6, -1, -1): show(n) print("---")
The range(6, -1, -1) goes 6, 5, 4, 3, 2, 1, 0 — start at 6, stop before -1, step by -1. Classic countdown range from PY-L1-13.
Mini-Challenge · Devi's Off-by-One Gallows
8 minDevi's Hangman draws the wrong picture every time. At full lives she's already showing a head. At one life left, the player is already dead. Find the bug.
# devi_hangman.py — buggy gallows
# (GALLOWS list ordered 6=empty, 5=head, ..., 0=dead — REVERSE of what we taught)
GALLOWS = [empty, head, head_body, plus_arm, plus_arms, plus_leg, dead]
# 0 1 2 3 4 5 6
def draw_gallows(lives):
print(GALLOWS[lives])
# main program
lives = 6
draw_gallows(lives) # prints dead — but we still have 6 lives!The bug is in the list ordering. Devi put the empty gallows at index 0 and the dead one at index 6 — the opposite of what we want.
- Fix 1 (simple). Reverse the list so index 0 = dead and index 6 = empty. Then
GALLOWS[lives]works as a direct lookup. - Fix 2 (math). Keep the list as-is and rewrite the function to
print(GALLOWS[LIVES - lives]). Works but harder to read.
Show the recommended fix
# devi_hangman.py — fixed by reordering GALLOWS = [dead, plus_leg, plus_arms, plus_arm, head_body, head, empty] # 0 1 2 3 4 5 6 def draw_gallows(lives): print(GALLOWS[lives])
Choosing the data shape to match the indexer is almost always cleaner than doing arithmetic on every access. Save the arithmetic for cases where you can't change the data structure.
Recap
3 minToday added no new keywords — only a beautifully tidy use of what you already knew. Triple-quoted strings hold multi-line ASCII art with newlines preserved. A list of seven such strings, ordered so that index equals the number of lives left, lets us pick the right picture with a single GALLOWS[lives] lookup. We wrapped that in a draw_gallows function so the main loop stays clean. The mechanics of the game are identical to Part 2; the experience is transformed because the feedback is now visual. Next lesson — Project: ASCII Banner Generator — we make our own ASCII art from scratch instead of looking it up.
Vocabulary Card
- triple-quoted string
- A string written with
"""...""". Newlines and indentation inside are preserved exactly. - list-as-lookup-table
- Storing values in a list so the index is the meaningful key. Common when the key is a small whole number.
- escape character
- The backslash
\\. To put a literal backslash in a string, type two:"\\\\". - visual feedback
- Showing the player what changed after their action. Often the difference between a script and a game.
- data-driven UI
- The display is computed from a value (here,
lives) rather than written separately for each case.
Homework
4 minUpdate your hangman.py to the full Part 3 version above. Two extras tonight:
- Add an
encouragementlist of three short messages — for example["Tough one!", "Keep going.", "You've got this."]. Pick one at random withrandom.choice(encouragement)after every wrong guess and print it. - Replace the dashes in your mask with spaces between them for readability — instead of
-a-a-a, print- a - a - a. Hint:" ".join(mask)from PY-L1-23. (You can do this at print-time only — don't change the actualmaskvariable.)
Bring hangman.py next class — PY-L1-38 starts the ASCII Banner Generator project.
Sample · the two homework upgrades
encouragement = ["Tough one!", "Keep going.", "You've got this.", "Don't give up — letters left to try!"] # inside the loop, on a wrong guess: if mask == old_mask: lives = lives - 1 draw_gallows(lives) print("❌ Nope.", random.choice(encouragement)) print("Lives left:", lives) # wherever you currently print the mask, use the join version: print("Word:", " ".join(mask))
Two tiny upgrades, one big polish boost. random.choice on a short list of messages adds variety without complicating the code. " ".join(mask) is the inverse of .split — it stitches the characters of a string together with spaces between. Both ideas appear again in pretty much every future game you'll write.