# Build a Hangman Game (Python)

> Build the classic word-guessing game in Python - masked word, guessed letters, dwindling lives, win/lose - running right in your browser (no input(), the play is simulated).


---

# Build a Hangman Game (Python)

You know Hangman. A hidden word shows up as a row of blanks. You guess a letter.
Right guesses fill in the blanks; wrong ones cost you a life. Run out of lives
and you lose. Fill in the word and you win.

It's a small game, but it touches a lot of the things you'll use forever in
Python: strings, sets, loops, conditionals, and a couple of functions that each
do one clear job. By the end you'll have built the whole thing, piece by piece,
and watched each piece run.

## What you'll build

A complete, playable round of Hangman: a secret word shown as blanks, a record of
which letters have been guessed, a life counter that drops on every miss, and the
two endings - you won, or you're out of lives.

## The stack

Python's standard library. Nothing to install. We use `random` for picking a word
and that's the only import in the whole project.

## This one runs in your browser

Every code block on these pages has a Run button. Click it and the code runs
right here - no setup, no terminal. Because there's no console to type into, we
don't use `input()`. Instead we feed the game a hardcoded list of guesses and
print the play-by-play, so you watch a full round unfold each time you run it.

In the last phase we'll also show you how to bolt on a real `input()` version so
you can play it for real on your own machine.

## Rough time

About 45 minutes if you read along and run each block. Less if you're quick.

## What you'll learn

| Skill | Where it shows up |
|-------|-------------------|
| Strings and slicing | Showing the word as blanks |
| Sets | Tracking guessed letters without duplicates |
| Loops | Walking the word, running the rounds |
| Conditionals | Hit vs miss, win vs lose |
| Functions | One job each: show, guess, check for a win |
| `random` | Picking a word from a list |

## The plan

```mermaid
flowchart LR
  A[Phase 1<br/>word as blanks] --> B[Phase 2<br/>handle a guess]
  B --> C[Phase 3<br/>lives and endings]
  C --> D[Phase 4<br/>word list and loop]
```

Each phase ends with a working piece you can run. By Phase 4 those pieces snap
together into the finished game. Let's pick a word.


---

# The Secret Word and the Blanks

The heart of Hangman is a single visual: a word you can't see yet, shown as a row
of blanks, with the letters you've correctly guessed filled in. Get that one
thing working and the rest of the game is steps you hang off it.

So that's where we start. A secret word, a set of letters that have been guessed,
and a way to print the word with the right letters showing and the rest hidden.

## The pieces we need

Two things:

- The **word** - a plain string, like `"python"`.
- The **guessed letters** - the letters the player has tried and got right (or
  tried at all; we'll firm that up next phase). For now, think of it as a small
  collection of single letters.

The display rule is one sentence: for each letter in the word, show the letter if
it's been guessed, otherwise show an underscore.

## Walking the word one letter at a time

In Python you can loop straight over a string and you get one character at a
time:

Before you run this, guess how many lines it'll print. Then check.

```python runnable
word = "python"
for letter in word:
    print(letter)
```

Run that. Six lines, one letter each. That `for letter in word` loop is the
engine of the whole display - we'll decide, per letter, whether to show it or
hide it.

## Show it or hide it

For each letter we want a small either/or: the real letter if it's been guessed,
an underscore if it hasn't. You already have the tools for that - an `if` check
and the `in` operator, which asks "is this letter inside the guessed collection?"
and hands back `True` or `False`. Put those together, loop over the word, and
you can build the whole display.

## Putting it together

Write a function `show(word, guessed)` that returns the word as a single display
string: each letter of `word`, in order, separated by spaces - the letter itself
if it's in `guessed`, an underscore if it isn't. `p _ t _ _ n` reads better than
`p_t__n`, which is why the spaces matter.

**Your turn.** This function is the point of the phase, so have a go before you
read on. Fill it in and hit Run: the checks underneath tell you whether it
works. My version is in the next block whenever you want it.

```python runnable
def show(word, guessed):
    # Return `word` as a display string: each letter, in order, separated
    # by single spaces. Show the letter if it's in `guessed`, otherwise
    # show "_".
    pass


# --- checks: fix your function until this prints "All good." ---
assert show("python", {"p", "t", "n"}) == "p _ t _ _ n", f"got: {show('python', {'p','t','n'})!r}"
assert show("python", set()) == "_ _ _ _ _ _", f"got: {show('python', set())!r}"
assert show("python", set("python")) == "p y t h o n", f"got: {show('python', set('python'))!r}"
print("All good.")
```

Stuck on the per-letter decision? You need one of two values for each letter -
think about how to say "this if a condition holds, otherwise that" in a single
line, then join the results with a space.

### One way to write it

Here's a first pass, glueing the per-letter results into one line by hand. Build
a list of the shown characters and join them with spaces:

```python runnable
word = "python"
guessed = {"p", "t", "n"}

shown = []
for letter in word:
    if letter in guessed:
        shown.append(letter)
    else:
        shown.append("_")

display = " ".join(shown)
print("Word:", display)
```

Run it. You should see `Word: p _ t _ _ n`. The `p`, `t`, and `n` show because
they're in `guessed`; the `y`, `h`, and `o` are still underscores. Change
`guessed` to `{"y", "o"}` and run again - different letters reveal. This is the
game's whole face.

That curly-brace `{"p", "t", "n"}` is a **set** - a collection with no duplicates
and fast membership checks. It's exactly the right tool for "which letters have
been guessed," and we'll lean on it hard next phase. For now, know that
`letter in guessed` against a set is quick and reads like English.

We'll need this display in every phase, so let's wrap it in a function that
takes the word and the guessed letters and hands back the line to print. Python
also has a compact way to write "this value if a condition is true, otherwise
that value," which flattens the loop above into one line:

```python
shown = letter if letter in guessed else "_"
```

Read it left to right: `letter` (use this) `if letter in guessed` (when it's
been guessed) `else "_"` (otherwise an underscore).

```python runnable
def show(word, guessed):
    return " ".join(letter if letter in guessed else "_" for letter in word)

# A few different guess states, so you can see the masking change:
word = "python"
print("Guessed p, t, n ->", show(word, {"p", "t", "n"}))
print("Guessed y, o    ->", show(word, {"y", "o"}))
print("Guessed nothing ->", show(word, set()))
print("Guessed it all  ->", show(word, set("python")))
```

Run it and read the four lines. The first two mask different letters. The third
passes an empty set (`set()`) - every letter is hidden, all underscores, exactly
what a fresh game looks like. The fourth passes `set("python")`, which turns the
string into a set of its letters `{'p','y','t','h','o','n'}` - every letter is
guessed, so the full word shows. That last line is what winning looks like, and
we'll use that same idea to detect a win in Phase 3.

## Why a set, not a list

You could store guessed letters in a list. A set is the better fit here for two
reasons:

| | List | Set |
|---|------|-----|
| Duplicate guesses | Keeps both | Ignores the repeat |
| "Have I guessed this?" | Scans the whole list | Instant lookup |

A player will fat-finger the same letter twice. A set absorbs that without you
writing a line of code to handle it. We'll make use of exactly that next.

## Where you are

You have a `show(word, guessed)` function that turns any word plus any set of
guessed letters into a clean masked line. That's the screen of your game. Next we
make a guess actually do something - add a letter, tell the player hit or miss,
and watch the blanks fill in.


---

# Handling a Guess

Right now the guessed letters are something you type into the code by hand. A real
game adds to them as the player guesses. This phase builds the part that takes a
single letter, records it, and reports back what happened: a hit, a miss, or a
letter the player already tried.

Carry the `show` function from Phase 1 with you - every example here re-declares
it so the blocks run on their own.

## The set does the bookkeeping

Guessed letters live in a set. Adding to a set is one method call, and a set
quietly drops duplicates, so a repeated guess never piles up:

Before you run this, guess what `len(guessed)` will print. Then check.

```python runnable
guessed = set()      # empty to start
guessed.add("p")
guessed.add("y")
guessed.add("p")     # repeat - the set ignores it
print(guessed)
print("Total letters tracked:", len(guessed))
```

Run it. You added `p` twice but the set holds two letters, not three. That's the
duplicate handling we talked about last phase, and it's free.

## Hit or miss

When a letter comes in, there are three outcomes:

```mermaid
flowchart TD
  A[letter guessed] --> B{already in guessed?}
  B -- yes --> R[repeat]
  B -- no --> C[add to guessed]
  C --> D{letter in word?}
  D -- yes --> H[hit]
  D -- no --> M[miss]
```

Read the diagram top to bottom. First we check if the letter was already tried -
if so, it's a repeat and nothing changes. Otherwise we record it, then check
whether it's actually in the word: in means hit, not in means miss.

## Writing the guess function

Write that flow as a function called `guess`. It takes the letter, the word, and
the guessed set, updates the set, and returns a short word describing what
happened. One wrinkle: a player might type `"P"` or `"p"` - your function should
treat them the same.

**Your turn.** This function is the point of the phase, so have a go before you
read on. Fill it in and hit Run: the checks underneath tell you whether it works.
My version is in the next block whenever you want it.

```python runnable
def guess(letter, word, guessed):
    # Record `letter` in `guessed` and report what happened. `letter` may
    # come in as "P" or "p" - treat them the same.
    #   - already in `guessed` -> return "repeat", don't touch anything else
    #   - otherwise add it (lowercased) to `guessed`, then return "hit" if
    #     it's in `word`, "miss" if it isn't
    pass


# --- checks: fix your function until this prints "All good." ---
guessed = set()
assert guess("P", "python", guessed) == "hit", f"got: {guess('P', 'python', set())!r}"
assert guessed == {"p"}, f"guessed should hold the lowercase letter, got: {guessed!r}"
assert guess("z", "python", guessed) == "miss", f"got: {guess('z', 'python', guessed)!r}"
assert guess("p", "python", guessed) == "repeat", f"got: {guess('p', 'python', guessed)!r}"
print("All good.")
```

Stuck on the `"P"` vs `"p"` wrinkle? There's a string method that hands back a
lowercase copy of any string - call it on `letter` before you do anything else
with it.

### One way to write it

```python runnable
def guess(letter, word, guessed):
    letter = letter.lower()          # treat P and p the same
    if letter in guessed:
        return "repeat"
    guessed.add(letter)
    return "hit" if letter in word else "miss"

# Try one guess and inspect the result:
word = "python"
guessed = set()
result = guess("P", word, guessed)
print("Result:", result)
print("Guessed so far:", guessed)
```

Run it. Notice we passed an uppercase `"P"` and the function lowercased it before
doing anything, so it matched the lowercase word and the set holds `'p'`. That
one `letter.lower()` line saves you from "I typed P and it said miss" complaints.

If you skipped the `.lower()` call, the checks above catch it fast: `guess("P",
"python", guessed)` returns `"miss"` instead of `"hit"`, because Python's `in`
check is case-sensitive and `"P"` is nowhere in the string `"python"`.

## Wiring it to the display

A guess on its own isn't satisfying - you want to *see* the word change. So after
each guess we print the result and the freshly masked word together. Here we
simulate a few guesses in a row, the way a real game would over several turns:

```python runnable
def show(word, guessed):
    return " ".join(letter if letter in guessed else "_" for letter in word)

def guess(letter, word, guessed):
    letter = letter.lower()
    if letter in guessed:
        return "repeat"
    guessed.add(letter)
    return "hit" if letter in word else "miss"

word = "python"
guessed = set()

# A simulated run of four turns (no input() - we feed the letters in):
for letter in ["p", "z", "y", "p"]:
    result = guess(letter, word, guessed)
    print(f"You guessed '{letter}': {result:6}  ->  {show(word, guessed)}")
```

Run it and read the four lines:

- `p` is a **hit** - the first blank fills in.
- `z` is a **miss** - `z` isn't in "python", the display doesn't change.
- `y` is a **hit** - another blank fills.
- `p` again is a **repeat** - we already had it, so nothing changes.

That `{result:6}` in the f-string pads the result word out to six characters so
the arrows line up in a neat column. A small touch, but a tidy readout is easier
to follow.

## Why we return a word instead of printing inside

You might wonder why `guess` returns `"hit"` / `"miss"` / `"repeat"` rather than
printing the message itself. Because the function's job is to *decide* what
happened, not to *display* it. The caller decides how to show it - maybe print it,
maybe count the misses, maybe both. Keeping "figure it out" separate from "show
it" means we can reuse `guess` in Phase 3 to drive the life counter without
touching it. One function, one job.

## A quick self-check

If the logic ever breaks, a tiny check catches it. This asserts the three
outcomes are what we expect, then prints a confirmation:

```python runnable
def guess(letter, word, guessed):
    letter = letter.lower()
    if letter in guessed:
        return "repeat"
    guessed.add(letter)
    return "hit" if letter in word else "miss"

g = set()
assert guess("p", "python", g) == "hit"
assert guess("z", "python", g) == "miss"
assert guess("p", "python", g) == "repeat"
print("All three outcomes behave. Guess logic is solid.")
```

Run it. If you see the confirmation line, the guess handling works. If an assert
ever failed, Python would stop and point at the broken line - a fast way to know
the moment something's off.

## Where you are

You can hand the game a letter and it does the right thing: records it, ignores
repeats, and tells you hit or miss while the masked word updates. What's missing
is stakes - a miss should cost something, and the game should end. That's next:
lives, winning, and losing.


---

# Lives, Wins, and Losses

A guessing game with no consequences is a list of letters. The fun is the tension:
every wrong guess brings you closer to losing, every right one closer to winning.
This phase adds that tension - a life counter that drops on misses, plus the two
checks that end the game.

By the end you'll run a complete round from start to finish.

## Lives are a number that only misses touch

A life is a counter. Start it at some limit - six is the classic Hangman number,
one for each part of the stick figure - and subtract one every time the player
misses. Hits don't touch it.

Before you run this, guess what `lives` will be after the loop. Then check.

```python runnable
lives = 6
# Simulate three misses:
for bad in ["z", "q", "x"]:
    lives -= 1
    print(f"Missed '{bad}'. Lives left: {lives}")
```

Run it. Three misses, three down from six, lands on three. When `lives` hits zero,
the player is out - that's the loss condition, and it's a plain `lives <= 0`
check.

## Detecting a win

A win is "every letter in the word has been guessed." We saw the shape of this in
Phase 1. Python's `all(...)` is built for exactly this question: give it a
sequence of true/false answers and it returns `True` only if every one is true.

```python runnable
def won(word, guessed):
    return all(letter in guessed for letter in word)

word = "python"
print("Half guessed:", won(word, {"p", "y", "t"}))
print("All guessed: ", won(word, set("python")))
```

Run it. The first line is `False` - `h`, `o`, and `n` are still missing. The
second is `True` - `set("python")` holds every letter, so every `letter in
guessed` check is true, so `all(...)` is true. That `won` function is your win
detector.

One thing worth noticing: this checks every *distinct* letter in the word, and a
word like "coffee" has repeats. That's fine - guessing `f` once puts `f` in the
set, and both `f` positions in "coffee" now count as guessed. The set handles
repeated letters in the word without any extra code.

## The shape of a round

Now we have all four ideas: show the word, take a guess, drop a life on a miss,
and check for a win or a loss. A round is a loop over the player's guesses that
stops the moment someone wins or runs out of lives.

```mermaid
flowchart TD
  A[next guess] --> B{won or no lives?}
  B -- yes --> E[round over]
  B -- no --> C[record guess]
  C --> D{hit?}
  D -- yes --> A
  D -- no --> F[lose a life] --> A
```

Read it: before each guess we check whether the game's already decided. If not, we
record the guess; a hit loops straight back for the next one, a miss costs a life
first. When the word's complete or the lives are gone, the round ends.

## A full round, start to finish

Here's the whole thing in one runnable block. Because there's no console to type
into, we feed it a hardcoded list of guesses and print every turn, then announce
the ending:

```python runnable
def show(word, guessed):
    return " ".join(letter if letter in guessed else "_" for letter in word)

def won(word, guessed):
    return all(letter in guessed for letter in word)

word = "python"
guessed = set()
lives = 6

# A simulated game: one miss ('z'), the rest hits - this should win.
moves = ["p", "y", "z", "t", "h", "o", "n"]

print(f"Starting word: {show(word, guessed)}  (lives: {lives})")
print("-" * 40)

for letter in moves:
    if lives <= 0 or won(word, guessed):
        break                       # game already decided, stop reading guesses
    guessed.add(letter)
    if letter in word:
        print(f"'{letter}' hit   | lives: {lives} | {show(word, guessed)}")
    else:
        lives -= 1
        print(f"'{letter}' miss  | lives: {lives} | {show(word, guessed)}")

print("-" * 40)
if won(word, guessed):
    print(f"You won! The word was '{word}'.")
else:
    print(f"Out of lives. The word was '{word}'.")
```

Run it. You'll watch the blanks fill in turn by turn, the single `z` miss knock a
life off, and the round end with `You won!`. That's a complete, playable round of
Hangman.

## Make it lose

Change the story and the same code handles the other ending. Swap the `moves` line
for a run of bad guesses and run it again:

```python runnable
def show(word, guessed):
    return " ".join(letter if letter in guessed else "_" for letter in word)

def won(word, guessed):
    return all(letter in guessed for letter in word)

word = "python"
guessed = set()
lives = 3                           # short life count, to lose fast

moves = ["z", "q", "x", "k", "b"]   # five misses, only three lives

for letter in moves:
    if lives <= 0 or won(word, guessed):
        break
    guessed.add(letter)
    if letter in word:
        print(f"'{letter}' hit   | lives: {lives} | {show(word, guessed)}")
    else:
        lives -= 1
        print(f"'{letter}' miss  | lives: {lives} | {show(word, guessed)}")

if won(word, guessed):
    print(f"You won! The word was '{word}'.")
else:
    print(f"Out of lives. The word was '{word}'.")
```

Run it. Three misses burn the three lives, and the loop's `break` fires before the
fourth guess is even read - notice only three turns print, then `Out of lives`.
Same engine, both endings, decided by the guesses and the life limit.

## Where you are

You have a full round: a masked word, guess handling, a life counter, and both
endings. The one thing still hardcoded is the word itself - you've been playing
"python" every time. Last phase we pick from a word list and wrap everything into
a single tidy game function, then point you at ways to make it your own.


---

# A Word List and the Game Loop

You've built every part: the masked word, the guess handler, the life counter,
and both endings. Two things finish the game. First, the word should vary - a
fixed "python" gets old. Second, all those parts should live in one function you
call to play a round. This phase does both, then hands you the keys to extend it.

## A list of words and a random pick

A word list is a plain list of strings. To grab one at random, the standard
library's `random` module has `choice`, which returns one item from a list:

```python runnable
import random

WORDS = ["python", "guitar", "rocket", "garden", "puzzle", "coffee"]
word = random.choice(WORDS)
print("Picked:", word)
```

Run it a few times - you'll get different words. That randomness is what makes the
game replayable. In the runnable blocks below we'll call `random.seed(...)` first
so the "random" pick is the same every run, which keeps the printed play-through
stable for you to read. On your own machine you'd drop the seed and let it be
genuinely random.

## Everything in one function

Here's the move that ties the project together: take the full round from Phase 3
and wrap it in a `play(word, moves, lives)` function. Same logic - show, guess,
count, check - but now it's one callable thing. We pass in the guesses as `moves`
because there's no console here; on your machine you'd read them from the player.

```python runnable
import random

WORDS = ["python", "guitar", "rocket", "garden", "puzzle", "coffee"]

def show(word, guessed):
    return " ".join(letter if letter in guessed else "_" for letter in word)

def won(word, guessed):
    return all(letter in guessed for letter in word)

def play(word, moves, lives=6):
    guessed = set()
    print(f"Secret word has {len(word)} letters: {show(word, guessed)}")
    for letter in moves:
        if lives <= 0 or won(word, guessed):
            break
        letter = letter.lower()
        if letter in guessed:
            print(f"'{letter}' already tried, skip")
            continue
        guessed.add(letter)
        if letter in word:
            print(f"'{letter}' hit   | lives: {lives} | {show(word, guessed)}")
        else:
            lives -= 1
            print(f"'{letter}' miss  | lives: {lives} | {show(word, guessed)}")
    print()
    if won(word, guessed):
        print(f"You won! The word was '{word}'.")
    else:
        print(f"Out of lives. The word was '{word}'.")

random.seed(2)                      # fixed pick so this run is repeatable
word = random.choice(WORDS)
moves = ["e", "a", "o", "p", "y", "t", "h", "n"]   # a simulated playthrough
play(word, moves)
```

Run it. With seed 2 the word comes out "python", and the move list starts with two
common-but-wrong vowels (`e`, `a`) before the right letters come in - so you watch
two lives burn, then the word fill in for a win. That's the finished game: random
word, a full round, both endings, all in one function.

The `default lives=6` in the signature means you can call `play(word, moves)` and
get six lives, or `play(word, moves, lives=3)` for a harder game, without changing
the function.

## A quick self-check

One assert-based check confirms the win and loss paths both work, so you know the
finished `play` logic is sound:

```python runnable
def won(word, guessed):
    return all(letter in guessed for letter in word)

def round_result(word, moves, lives):
    guessed = set()
    for letter in moves:
        if lives <= 0 or won(word, guessed):
            break
        guessed.add(letter.lower())
        if letter.lower() not in word:
            lives -= 1
    return "win" if won(word, guessed) else "lose"

assert round_result("python", ["p","y","t","h","o","n"], 6) == "win"
assert round_result("python", ["z","q","x","k"], 3) == "lose"
print("Win path and lose path both check out. Game logic is solid.")
```

Run it. The confirmation line means a clean sweep wins and three misses on three
lives loses - the two outcomes your `play` function rests on.

## Where to take it

You have a complete game. Here are real extensions, easiest first:

| Idea | The gist |
|------|----------|
| Bigger word list | Add words to `WORDS`. The game gets them for free. |
| Categories | Make `WORDS` a dict like `{"animals": [...], "fruit": [...]}` and pick a category, then a word. |
| A hint | After a few misses, reveal one unguessed letter: pick from `set(word) - guessed`. |
| Score | Count wins across rounds; award points for lives remaining at the win. |
| ASCII art | Print a stick figure that grows a limb per miss - a list of art strings indexed by misses. |

## Play it for real on your machine

The browser version simulates guesses because there's no place to type. On your
own machine you can read real input. Save this as `hangman.py` and run it with
`python hangman.py` in your terminal - it asks for one letter per turn:

```python
import random

WORDS = ["python", "guitar", "rocket", "garden", "puzzle", "coffee"]

def show(word, guessed):
    return " ".join(letter if letter in guessed else "_" for letter in word)

def won(word, guessed):
    return all(letter in guessed for letter in word)

def play():
    word = random.choice(WORDS)
    guessed = set()
    lives = 6
    while lives > 0 and not won(word, guessed):
        print(f"\n{show(word, guessed)}    lives: {lives}")
        letter = input("Guess a letter: ").strip().lower()
        if not letter or len(letter) != 1 or not letter.isalpha():
            print("Type a single letter.")
            continue
        if letter in guessed:
            print("Already tried that one.")
            continue
        guessed.add(letter)
        if letter in word:
            print("Hit!")
        else:
            lives -= 1
            print("Miss.")
    if won(word, guessed):
        print(f"\nYou won! The word was '{word}'.")
    else:
        print(f"\nOut of lives. The word was '{word}'.")

if __name__ == "__main__":
    play()
```

It's the same game you built here - `show`, `won`, the life count, both endings.
The only change is the `while` loop reads a real guess with `input()` instead of
walking a fixed list, plus a check that the player typed exactly one letter. That
input-guard is the kind of thing you skip in a demo and want the moment a real
person uses it.

## You built it

A row of blanks became a guess handler became a life counter became a finished,
random, replayable game - each phase a piece you ran and watched work. That's the
whole arc of building software: small correct parts, snapped together. Now go add
a category mode, or that growing stick figure, and make it yours.
