Skip to content

Guess the Number Game

“Guess the Number” is the rite-of-passage game in every beginner curriculum, and for good reason: in a single short script you exercise nearly every fundamental — randomness, looping, branching, user input, type conversion, comparisons, and string formatting. The premise: the computer secretly chooses a number between 1 and 20, the user has six chances to guess it, and after every guess the program tells them whether they were too high or too low.

In this tutorial we will:

  • Build the classic version from scratch.
  • Walk line-by-line through what each statement does.
  • Harden the program against bad input (letters, negative numbers, empty input).
  • Extend it with difficulty levels, score tracking, and a “play again” loop.
  • Python 3.6 or above.
  • A text editor or IDE.
  • Familiarity with running a Python script (see Hello World).
  • Some comfort with input(), print(), and basic comparisons.
  • random.randint(a, b) — picks a random integer between a and b, inclusive.
  • for loop with range — iterates a fixed number of times.
  • if/elif/else — choose between several branches.
  • break — exit a loop immediately.
  • Type conversionint(input(...)) turns text into a number.
  • String concatenation vs. f-strings — two ways to insert values into messages.
  1. Make a folder named guess-the-number.
  2. Inside it, create a file named guessthenumber.py.
  3. Open the folder in your editor.

Paste the following into guessthenumber.py:

Guess The Number pch.viewSource
Guess The Number
"""Guess the number -- and a measurement of whether six guesses is enough.

The game gives the player 6 attempts at a number from 1 to 20. Whether that
is generous or stingy is not a matter of opinion: binary search needs at most
ceil(log2(20)) = 5 guesses, so a player who halves the range every time
cannot lose. The demo at the bottom plays both strategies 20,000 times and
prints the win rates.

    python guessthenumber.py           # play; unattended it runs the demo
    python guessthenumber.py --demo    # strategy comparison
    python guessthenumber.py --test    # unit tests
"""

import math
import random
import statistics
import sys

LOW, HIGH = 1, 20
MAX_GUESSES = 6


def ask(prompt="", default=""):
    """Read a line, or fall back to `default` when nobody is there to type."""
    try:
        return input(prompt).strip() or default
    except EOFError:
        print(f"{default}   (no input available, using the default)")
        return default


def ask_number(prompt="", default=1, low=LOW, high=HIGH):
    """Keep asking until the answer is a number inside the range.

    `int(input())` raises ValueError on "ten" and happily accepts 500 for a
    game about 1 to 20. Both are the same bug -- trusting the string -- and
    both cost the player a turn they did not use.
    """
    while True:
        raw = ask(prompt, str(default))
        try:
            value = int(raw)
        except ValueError:
            print(f"'{raw}' is not a whole number.")
            continue
        if not low <= value <= high:
            print(f"Pick something between {low} and {high}.")
            continue
        return value


def play(secret=None, name=None):
    """One game. Returns the number of guesses used, or None on a loss."""
    secret = random.randint(LOW, HIGH) if secret is None else secret
    if name is None:
        print("Hello, what is your name?")
        name = ask("", "Player")
    print(f"Well, {name}, I am thinking of a number between "
          f"{LOW} and {HIGH}.")

    for taken in range(1, MAX_GUESSES + 1):
        print(f"Take a guess. You have {MAX_GUESSES - taken + 1} left.")
        guess = ask_number("", (LOW + HIGH) // 2)
        if guess < secret:
            print("Your guess is too low.")
        elif guess > secret:
            print("Your guess is too high.")
        else:
            print(f"Good job, {name}! You guessed my number in "
                  f"{taken} guesses.")
            return taken
    print(f"Nope. The number I was thinking of was {secret}.")
    return None


def play_binary(secret):
    """Halve the range each time. Returns the guess count."""
    low, high = LOW, HIGH
    for taken in range(1, MAX_GUESSES + 1):
        guess = (low + high) // 2
        if guess == secret:
            return taken
        if guess < secret:
            low = guess + 1
        else:
            high = guess - 1
    return None


def play_random(secret):
    """Guess at random from the numbers not yet tried, ignoring the hints."""
    remaining = list(range(LOW, HIGH + 1))
    random.shuffle(remaining)
    for taken, guess in enumerate(remaining[:MAX_GUESSES], start=1):
        if guess == secret:
            return taken
    return None


def play_linear(secret):
    """Count up from 1. Correct, and hopeless past the sixth number."""
    for taken, guess in enumerate(range(LOW, HIGH + 1), start=1):
        if taken > MAX_GUESSES:
            return None
        if guess == secret:
            return taken
    return None


def measure(strategy, trials=20_000):
    random.seed(20260809)
    results = [strategy(random.randint(LOW, HIGH)) for _ in range(trials)]
    wins = [r for r in results if r is not None]
    return {
        "win_rate": len(wins) / trials,
        "mean_guesses": statistics.mean(wins) if wins else float("nan"),
        "worst": max(wins) if wins else None,
    }


def demo():
    bound = math.ceil(math.log2(HIGH - LOW + 1))
    print(f"{HIGH - LOW + 1} numbers, {MAX_GUESSES} guesses. "
          f"Binary search needs at most {bound}.\n")
    print(f"{'strategy':22} {'wins':>7} {'mean guesses':>14} {'worst':>7}")
    print("-" * 54)
    for label, strategy in (("binary search", play_binary),
                            ("random, no repeats", play_random),
                            ("count up from 1", play_linear)):
        stats = measure(strategy)
        print(f"{label:22} {stats['win_rate']:6.1%} "
              f"{stats['mean_guesses']:14.2f} {stats['worst']:>7}")
    print("\nBinary search never loses, because 6 guesses is one more than it")
    print("needs. The hints -- higher, lower -- are the entire game: a player")
    print("who ignores them is left drawing from 20 numbers with 6 tickets.")


if __name__ == "__main__":
    if "--test" in sys.argv:
        import unittest

        class TestGame(unittest.TestCase):
            def test_binary_search_always_wins(self):
                for secret in range(LOW, HIGH + 1):
                    self.assertIsNotNone(play_binary(secret))

            def test_binary_search_within_bound(self):
                bound = math.ceil(math.log2(HIGH - LOW + 1))
                worst = max(play_binary(s) for s in range(LOW, HIGH + 1))
                self.assertLessEqual(worst, bound)

            def test_linear_loses_past_the_limit(self):
                self.assertIsNone(play_linear(HIGH))
                self.assertEqual(play_linear(LOW), 1)

        unittest.main(argv=sys.argv[:1], exit=False)
    elif "--demo" in sys.argv:
        demo()
    else:
        play()
        print()
        demo()

Save, open a terminal in the folder, and run:

command
C:\Users\username\Documents\guess-the-number> python guessthenumber.py
Hello, what is your name?
Ravi
Well, Ravi, I am thinking of a number between 1 and 20.
Take a guess. You have 6 guesses left.
23
Your guess is too high.
Take a guess. You have 5 guesses left.
12
Your guess is too high.
Take a guess. You have 4 guesses left.
11
Your guess is too high.
Take a guess. You have 3 guesses left.
7
Your guess is too high.
Take a guess. You have 2 guesses left.
5
Good job, Ravi! You guessed my number in 5 guesses.

Run it a few times. The secret number changes every time.

Running the file exactly as it ships takes 0.3 s and prints:

python guessthenumber.py
Hello, what is your name?
Player   (no input available, using the default)
Well, Player, I am thinking of a number between 1 and 20.
Take a guess. You have 6 left.
10   (no input available, using the default)
Your guess is too high.
Take a guess. You have 5 left.
10   (no input available, using the default)
Your guess is too high.
Take a guess. You have 4 left.
10   (no input available, using the default)
Your guess is too high.
Take a guess. You have 3 left.
10   (no input available, using the default)
Your guess is too high.
Take a guess. You have 2 left.
10   (no input available, using the default)
Your guess is too high.
Take a guess. You have 1 left.
10   (no input available, using the default)
...

The first 20 of 34 lines are shown; the run continues past this point.

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid
guessthenumber.py
import random

The random module is part of Python’s standard library. After this line, random.randint(...) is available.

guessthenumber.py
print('Hello, what is your name?')
name = input()
print('Well, ' + name + ', I am thinking of a number between 1 and 20.')
  • input() waits for the user to press Enter and returns their text as a string.
  • 'Well, ' + name + ', ...' is string concatenation — joining pieces of text with +.

💡 A more modern alternative is the f-string: print(f'Well, {name}, I am thinking of a number between 1 and 20.')

guessthenumber.py
secretNumber = random.randint(1, 20)

random.randint(1, 20) returns a random integer from 1 to 20 inclusive — both endpoints are possible.

guessthenumber.py
for guessesTaken in range(1, 7):
    print('Take a guess. You have ' + str(7 - guessesTaken) + ' guesses left.')
    guess = int(input())
  • range(1, 7) generates 1, 2, 3, 4, 5, 6 — six iterations.
  • guessesTaken is the current attempt number.
  • 7 - guessesTaken gives the player how many tries remain.
  • int(input()) reads a line and converts it to an integer. If the user types "abc", this line will crash — we will fix that in the “Robust Input” section below.
guessthenumber.py
    if guess < secretNumber:
        print('Your guess is too low.')
    elif guess > secretNumber:
        print('Your guess is too high.')
    else:
        break

The three branches cover every possibility:

  • < → too low, keep going.
  • > → too high, keep going.
  • otherwise (==) → correct, break out of the loop.
guessthenumber.py
if guess == secretNumber:
    print('Good job, ' + name + '! You guessed my number in ' + str(guessesTaken) + ' guesses.')
else:
    print('Nope. The number I was thinking of was ' + str(secretNumber))

If the loop ended because of break, the player won. Otherwise they ran out of guesses.

Strategy: How Many Guesses Should You Need?

Section titled “Strategy: How Many Guesses Should You Need?”

With six guesses for a range of 20, the optimal strategy is binary search — always guess the middle. Each guess halves the remaining range:

Guess #RangeMid
11–2010
2half5 or 15
3quarter
4–5usually solved

In general you need about log₂(n) guesses for a range of n numbers. For 1–20 that is about 4.3, so six guesses is generous.

The basic version crashes when the user types letters. Replace int(input()) with a helper:

safe_input.py
def ask_number(prompt, low, high):
    while True:
        raw = input(prompt)
        try:
            n = int(raw)
        except ValueError:
            print("Please enter a whole number.")
            continue
        if not (low <= n <= high):
            print(f"Number must be between {low} and {high}.")
            continue
        return n

Now guess = ask_number("Your guess: ", 1, 20) handles every bad input gracefully.

ProblemCauseFix
ValueError: invalid literal for int()User typed lettersWrap in try/except ValueError
Loop ends but program says “Nope” even when correctUsed == inside the loop without breakAdd break in the else branch
Cannot concatenate str and int'left: ' + guessesTakenConvert with str(guessesTaken) or use an f-string
Random number is always the sameYou set random.seed(0) somewhereRemove the seed call for true variability
difficulty.py
levels = {"easy": (1, 10, 6), "medium": (1, 50, 7), "hard": (1, 100, 8)}
choice = input("Difficulty (easy/medium/hard): ").lower()
low, high, tries = levels[choice]
secret = random.randint(low, high)
replay.py
while True:
    play_game()
    again = input("Play again? (y/n): ").lower()
    if again != "y":
        break
best.py
best = None
# after each round:
if best is None or guessesTaken < best:
    best = guessesTaken
    print(f"New best score: {best} guesses!")
persist.py
from pathlib import Path
SCORE_FILE = Path("best_score.txt")
best = int(SCORE_FILE.read_text()) if SCORE_FILE.exists() else None
# update and write back:
if best is None or new_score < best:
    SCORE_FILE.write_text(str(new_score))

5. Reverse the game — you pick, the computer guesses

Section titled “5. Reverse the game — you pick, the computer guesses”

See the Number Guessing Game with AI project. The AI uses binary search to guess your number in at most seven tries.

Replace input() and print() with an entry widget and a label. A few hours of work and you have a clickable game.

Common Interview-Style Questions This Project Touches

Section titled “Common Interview-Style Questions This Project Touches”
  • “How would you guess a number between 1 and 1,000,000 with the fewest tries?” → binary search.
  • “Why is int(input()) dangerous?” → invalid user input can crash the program; use try/except.
  • “How does Python’s random module work under the hood?” → Mersenne Twister, pseudo-random.
  • Game development (loot tables, RNG events, procedural generation).
  • Educational tools that teach the binary search algorithm visually.
  • A/B testing assignment when you need a random bucket.
  • Quick demos of probability concepts in classrooms.

Pick one of the variations above and ship it. Combine two or three for extra credit — for example, difficulty levels + persistent high-score + play-again loop is a complete tiny game. Then move on to projects that combine randomness with state, such as Hangman or Blackjack.

The computer picked a secret number from 1–100. Click the bar to guess — it tells you higher or lower and narrows the shaded range until you find it:

sketch Guess the number (1-100) p5.js
Click the bar to guess. The game says higher or lower and narrows the range until you win.

You wrote a complete game in roughly fifteen lines of Python, learned how random.randint works, looped with forrange, branched with if/elif/else, and saw how a small program scales up with input validation and replayability. The full source is on GitHub. Find more projects like this on Python Central Hub.

  • int(input()) crashes on anything that is not a number. Typing ten raises ValueError and ends the game. ask_number in the shipped file loops until the answer parses and falls inside 1–20, because accepting 500 in a game about 1 to 20 is the same bug wearing a different hat.
  • Six guesses is not a difficulty setting. Binary search needs at most 5 for a range of 20, so the game is unlosable for a player who halves — measured at 100.0% over 20,000 rounds against 30.3% for a player who ignores the hints.
  • guess may be unbound. The original version referred to guess after the loop; if the loop body had never assigned it, that is a NameError rather than a loss.
  • Comparing the string, not the number. "10" < "9" is True, because string comparison is lexicographic. Any hint logic that forgets the int() gives confidently wrong higher-or-lower answers.
  • A new random number per guess. Calling randint inside the loop instead of before it makes the game unwinnable and the bug invisible — it still runs, it just never says you won.
  • Range 1–20, 6 guesses, measured over 20,000 rounds each:
strategywinsmean guessesworst
binary search100.0%3.705
random, no repeats30.3%3.516
count up from 129.8%3.506
  • The mean guess count is lower for the losing strategies, because their losses are not counted — a mean over survivors only is a trap worth recognising.
  • Binary search costs ceil(log2 n) guesses: 5 for 20, 7 for 100, 20 for a million.
  • The higher/lower hints are the entire game. Without them it is a lottery with 6 tickets out of 20.
pch.quizTag pch.quizDefaultTitle
  1. The game allows 6 guesses for a number from 1 to 20. How hard is that for a player using binary search?

    pch.quizShowAnswer

    B — Impossible to lose: binary search needs at most ceil(log2 20) = 5, so the sixth guess is spare

  2. Random guessing without repeats won 30.3% of the time, and its mean winning guess count was 3.51 — lower than binary search's 3.70. Is random guessing better?

    pch.quizShowAnswer

    B — No — the mean only counts games it won. The 69.7% it lost contribute nothing to that average, so the two numbers are not comparable

  3. Why does raising the range from 1-1,000 to 1-1,000,000 cost binary search only 10 extra guesses?

    pch.quizShowAnswer

    B — Each guess halves the candidates, so the cost is log2 of the range: 10 guesses for a thousand, 20 for a million

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading