Guess the Number Game
Abstract
Section titled “Abstract”“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.
Prerequisites
Section titled “Prerequisites”- 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.
Concepts You Will Use
Section titled “Concepts You Will Use”random.randint(a, b)— picks a random integer betweenaandb, inclusive.forloop withrange— iterates a fixed number of times.if/elif/else— choose between several branches.break— exit a loop immediately.- Type conversion —
int(input(...))turns text into a number. - String concatenation vs. f-strings — two ways to insert values into messages.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Make a folder named
guess-the-number. - Inside it, create a file named
guessthenumber.py. - Open the folder in your editor.
Write the code
Section titled “Write the code”Paste the following into guessthenumber.py:
Guess The Number
pch.viewSource"""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:
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.
What it produces
Section titled “What it produces”Running the file exactly as it ships takes 0.3 s and prints:
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.
How it fits together
Section titled “How it fits together”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.
flowchart TD
RUN(["python guessthenumber.py"])
ask("ask")
RUN --> ask
Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. Import randomness
Section titled “1. Import randomness”import randomThe random module is part of Python’s standard library. After this line, random.randint(...) is available.
2. Greet the user
Section titled “2. Greet the user”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.')
3. Pick a secret number
Section titled “3. Pick a secret number”secretNumber = random.randint(1, 20)random.randint(1, 20) returns a random integer from 1 to 20 inclusive — both endpoints are possible.
4. Loop six times
Section titled “4. Loop six times”for guessesTaken in range(1, 7):
print('Take a guess. You have ' + str(7 - guessesTaken) + ' guesses left.')
guess = int(input())range(1, 7)generates1, 2, 3, 4, 5, 6— six iterations.guessesTakenis the current attempt number.7 - guessesTakengives 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.
5. Compare and react
Section titled “5. Compare and react” if guess < secretNumber:
print('Your guess is too low.')
elif guess > secretNumber:
print('Your guess is too high.')
else:
breakThe three branches cover every possibility:
<→ too low, keep going.>→ too high, keep going.- otherwise (
==) → correct,breakout of the loop.
6. Final message
Section titled “6. Final message”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 # | Range | Mid |
|---|---|---|
| 1 | 1–20 | 10 |
| 2 | half | 5 or 15 |
| 3 | quarter | … |
| 4–5 | usually 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.
Robust Input
Section titled “Robust Input”The basic version crashes when the user types letters. Replace int(input()) with a helper:
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 nNow guess = ask_number("Your guess: ", 1, 20) handles every bad input gracefully.
Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
ValueError: invalid literal for int() | User typed letters | Wrap in try/except ValueError |
| Loop ends but program says “Nope” even when correct | Used == inside the loop without break | Add break in the else branch |
Cannot concatenate str and int | 'left: ' + guessesTaken | Convert with str(guessesTaken) or use an f-string |
| Random number is always the same | You set random.seed(0) somewhere | Remove the seed call for true variability |
Variations to Try
Section titled “Variations to Try”1. Difficulty levels
Section titled “1. Difficulty levels”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)2. Play-again loop
Section titled “2. Play-again loop”while True:
play_game()
again = input("Play again? (y/n): ").lower()
if again != "y":
break3. Track best score
Section titled “3. Track best score”best = None
# after each round:
if best is None or guessesTaken < best:
best = guessesTaken
print(f"New best score: {best} guesses!")4. Persistent high-score file
Section titled “4. Persistent high-score file”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.
6. GUI version with Tkinter
Section titled “6. GUI version with Tkinter”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; usetry/except. - “How does Python’s
randommodule work under the hood?” → Mersenne Twister, pseudo-random.
Real-World Applications
Section titled “Real-World Applications”- 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.
Next Steps
Section titled “Next Steps”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.
Try it here
Section titled “Try it here”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:
Conclusion
Section titled “Conclusion”You wrote a complete game in roughly fifteen lines of Python, learned how random.randint works, looped with for…range, 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.
Pitfalls
Section titled “Pitfalls”int(input())crashes on anything that is not a number. TypingtenraisesValueErrorand ends the game.ask_numberin 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.
guessmay be unbound. The original version referred toguessafter the loop; if the loop body had never assigned it, that is aNameErrorrather than a loss.- Comparing the string, not the number.
"10" < "9"isTrue, because string comparison is lexicographic. Any hint logic that forgets theint()gives confidently wrong higher-or-lower answers. - A new random number per guess. Calling
randintinside 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:
| strategy | wins | mean guesses | worst |
|---|---|---|---|
| binary search | 100.0% | 3.70 | 5 |
| random, no repeats | 30.3% | 3.51 | 6 |
| count up from 1 | 29.8% | 3.50 | 6 |
- 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.
-
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
-
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
-
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
Try it yourself
Section titled “Try it yourself”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading