A number guessing game is a compact project that combines random values, loops, conditions, functions, input validation, and game state. The computer chooses a secret number, and the player receives higher-or-lower hints until the answer is correct or the available attempts run out.
This tutorial builds a complete number guessing game in Python and explains each design decision. The final version supports difficulty levels, repeated guesses, input validation, a limited number of attempts, replay, and a testable hint function.
The project uses the Python random module to choose the secret value. It also applies clean function boundaries similar to those covered in the Python return statement guide.
How the Game Works
- The player chooses a difficulty.
- The computer generates a secret integer inside a range.
- The player enters a guess.
- The program validates the input.
- The game reports whether the guess is too low, too high, or correct.
- The round ends after a correct answer or the final attempt.
- The player may start another round.
The official Python random documentation defines randint(), and the input() documentation describes terminal input.
Create the Simplest Version
import random
secret = random.randint(1, 100)
guess = int(input("Guess a number from 1 to 100: "))
if guess == secret:
print("Correct!")
elif guess < secret:
print("Too low")
else:
print("Too high")This version demonstrates the core comparison, but it accepts only one guess and crashes when the input cannot be converted to an integer.
Repeat with a while Loop
import random
secret = random.randint(1, 100)
guess = None
while guess != secret:
guess = int(input("Your guess: "))
if guess < secret:
print("Too low")
elif guess > secret:
print("Too high")
print("You found it!")A sentinel value of None makes the first loop condition clear. The loop continues until the guess matches the secret.
Validate User Input
Terminal input arrives as text. Converting invalid text with int() raises ValueError. A dedicated function can repeat until it receives an integer within the permitted range.
def read_integer(prompt, minimum, maximum):
while True:
raw_value = input(prompt).strip()
try:
value = int(raw_value)
except ValueError:
print("Enter a whole number.")
continue
if not minimum <= value <= maximum:
print(f"Enter a number from {minimum} to {maximum}.")
continue
return valueThe Python errors and exceptions tutorial explains why specific exceptions should be handled near the operation that can fail.
Add Limited Attempts
A limited number of attempts gives the game a clear losing condition. A for loop naturally represents a fixed number of turns.
MAX_ATTEMPTS = 7
for attempt in range(1, MAX_ATTEMPTS + 1):
remaining = MAX_ATTEMPTS - attempt
guess = read_integer("Your guess: ", 1, 100)
if guess == secret:
print(f"Correct in {attempt} attempts!")
break
if guess < secret:
print("Too low")
else:
print("Too high")
print(f"Attempts remaining: {remaining}")
else:
print(f"The secret number was {secret}.")The else attached to the loop runs only when the loop finishes without break. This is a useful but sometimes overlooked Python feature.
Prevent Repeated Guesses
A set stores unique guesses and provides fast membership checks.
guessed_numbers = set()
guess = read_integer("Your guess: ", 1, 100)
if guess in guessed_numbers:
print("You already tried that number.")
else:
guessed_numbers.add(guess)Repeated guesses should not consume an attempt. The complete program handles this with a while loop whose attempt counter increases only after a new valid guess.
Create Reusable Hint Logic
Separating comparisons from terminal output makes the core rule easy to test.
def compare_guess(guess, secret):
if guess < secret:
return "low"
if guess > secret:
return "high"
return "correct"This small pure function always returns the same result for the same inputs. It has no terminal interaction and no random behavior.
Add Difficulty Levels
Store each mode in a dictionary:
DIFFICULTIES = {
"easy": {"maximum": 50, "attempts": 10},
"normal": {"maximum": 100, "attempts": 7},
"hard": {"maximum": 500, "attempts": 9},
}The hard mode uses a wider range and slightly more attempts. Difficulty does not have to mean only fewer turns; it can also change the available interval.
Complete Number Guessing Game
import random
DIFFICULTIES = {
"easy": {"maximum": 50, "attempts": 10},
"normal": {"maximum": 100, "attempts": 7},
"hard": {"maximum": 500, "attempts": 9},
}
def read_integer(prompt, minimum, maximum):
while True:
raw_value = input(prompt).strip()
try:
value = int(raw_value)
except ValueError:
print("Enter a whole number.")
continue
if minimum <= value <= maximum:
return value
print(f"Enter a number from {minimum} to {maximum}.")
def choose_difficulty():
while True:
print("Available levels: easy, normal, hard")
choice = input("Difficulty: ").strip().lower()
if choice in DIFFICULTIES:
return choice
print("Choose easy, normal, or hard.")
def compare_guess(guess, secret):
if guess < secret:
return "low"
if guess > secret:
return "high"
return "correct"
def play_round(generator=None):
generator = generator or random.Random()
difficulty = choose_difficulty()
settings = DIFFICULTIES[difficulty]
maximum = settings["maximum"]
max_attempts = settings["attempts"]
secret = generator.randint(1, maximum)
guessed_numbers = set()
attempt = 0
print(f"I chose a number from 1 to {maximum}.")
while attempt < max_attempts:
guess = read_integer("Your guess: ", 1, maximum)
if guess in guessed_numbers:
print("You already tried that number.")
continue
guessed_numbers.add(guess)
attempt += 1
result = compare_guess(guess, secret)
if result == "correct":
print(f"Correct! You used {attempt} attempts.")
return True
direction = "low" if result == "low" else "high"
remaining = max_attempts - attempt
print(f"Too {direction}. Attempts remaining: {remaining}")
print(f"No attempts left. The number was {secret}.")
return False
def wants_to_play_again():
while True:
answer = input("Play again? [y/n]: ").strip().lower()
if answer in {"y", "yes"}:
return True
if answer in {"n", "no"}:
return False
print("Enter y or n.")
def main():
print("Number Guessing Game")
while True:
play_round()
if not wants_to_play_again():
print("Thanks for playing!")
break
if __name__ == "__main__":
main()The if __name__ == "__main__" guard prevents the interactive game from starting when the file is imported by a test.
Test the Comparison Function
Create test_guessing_game.py:
from guessing_game import compare_guess
def test_compare_guess():
assert compare_guess(3, 8) == "low"
assert compare_guess(10, 8) == "high"
assert compare_guess(8, 8) == "correct"The unit testing guide covers test structure, mocks, and frameworks. You can also inject random.Random(42) into play_round() when a predictable sequence helps a test.
Debug the Game in VS Code
Set a breakpoint after the secret number is created, start a debug session, and inspect secret, attempt, and guessed_numbers. The VS Code Python debugging guide explains breakpoints and stepping.
Common Mistakes
- Generating a new secret number inside the guessing loop.
- Forgetting that
input()returns a string. - Using a broad
except Exceptioninstead of handlingValueError. - Counting invalid or repeated input as a real attempt.
- Creating an endless loop with no correct exit condition.
- Putting all code at module level, which makes testing difficult.
- Revealing the secret number in normal output before the round ends.
Ideas to Extend the Project
- Add a score based on remaining attempts.
- Save the best score to JSON.
- Offer warmer and colder hints based on distance.
- Add a two-player mode.
- Create a graphical version with Turtle or Pygame.
- Accept difficulty through command-line arguments.
- Record completed rounds with the Python logging module.
Frequently Asked Questions
Why use randint()?
It returns an integer and includes both boundaries, which matches a range such as 1 through 100.
Why store guesses in a set?
A set keeps unique values and makes repeated-guess checks clear.
How many attempts should the player receive?
It depends on the range and desired difficulty. Start with values that make the game enjoyable, then adjust after testing.
Can the secret be reproducible?
Yes. Use a dedicated random.Random(seed) instance for demos or automated tests. Do not use predictable seeds for security-sensitive data.
How do I avoid the game starting during tests?
Put the interactive entry point inside main() and call it under the __main__ guard.
Conclusion
A number guessing game in Python is small enough to understand in one sitting but rich enough to teach important structure. Random generation creates the challenge, validation protects the program from bad input, functions separate responsibilities, and tests verify the core rules.
After the basic version works, add one improvement at a time. Difficulty, scoring, saved results, and graphical interfaces all build naturally on the same clean game loop.






