Build a complete Python number guessing game from scratch. This step-by-step project ties together loops, conditionals, input validation, and the random module. It is your first real program, with tested code and real sample output.
“The best way to learn to program is to write programs.”
Brian Kernighan
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 14 minutes
What We’re Building
Time to stop writing isolated snippets and build something you can actually show people. We are making a Python guessing game. The computer picks a secret number between 1 and 100, and you try to guess it. After every guess, the game tells you whether you are too high or too low, and as you get closer it tells you that you are getting warm. Think of it like the “hotter or colder” game kids play to find a hidden object. Every step nudges you toward the answer.
Here’s what the finished game looks like when a player named Rahul gives it a spin:
▶ Sample Game Session
======================================== 🎮 WELCOME TO THE GUESSING GAME! ======================================== Enter your name: Rahul 🎯 NUMBER GUESSING GAME I'm thinking of a number between 1 and 100. You have 7 attempts. Let's go! Attempt 1/7, your guess: 50 ♨️ Warm! Try higher. Attempt 2/7, your guess: 75 🌡️ Getting there. Try lower. Attempt 3/7, your guess: 62 🔥 SO CLOSE! Just a little lower. Attempt 4/7, your guess: 56 🔥 SO CLOSE! Just a little higher. Attempt 5/7, your guess: 59 🎉 CORRECT! You got it in 5 attempts! 🏆 SCOREBOARD for Rahul This round: 5 attempts Play again? (yes/no): no Thanks for playing, Rahul! Games: 1 | Best: 5 attempts
Table of Contents
Why This Project
Learning Python from isolated examples is like practicing guitar chords one at a time: useful, but it never feels like music. This project is your first full song. It exercises every fundamental skill you’ve learned so far: while loops, if/elif/else, input() with type casting, import, f-strings, lists, and basic error handling. It’s small enough to finish in one sitting but complex enough to feel like a real program.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The flowchart maps the whole game loop: generate a random number, ask the player for a guess, check that the guess is valid, compare it to the secret number, then either drop a hint or celebrate the win. Notice the arrow that loops from the hint step back to the prompt step. That loop is the while structure you are about to build. The branch that checks the input is what keeps the game from crashing when someone types nonsense, and you will reuse that exact pattern in every interactive program you write.
Requirements
- Generate a random number between 1 and 100
- Give the player 7 attempts to guess it
- After each guess, say “too high” or “too low”
- Handle invalid input (letters, empty input, out-of-range numbers) without crashing
- Show how many attempts it took on success
- Allow replaying and track the best score
Step 1: Generate a Random Number
📄 step1_random.py: Using the random module
import random
secret = random.randint(1, 100)
print(f"(Debug) Secret number: {secret}")
print("I'm thinking of a number between 1 and 100.")
▶ Output (your number will differ)
(Debug) Secret number: 73 I'm thinking of a number between 1 and 100.
What happened here: random.randint(1, 100) hands you a random whole number from 1 to 100, and both ends are included, so 1 and 100 can both show up. Think of it like rolling a 100-sided die. We print the secret while we are still building so we can check that the rest of the game reacts correctly. That debug line goes away once everything works.
Step 2: Get Player Input with Validation
Players will type anything: letters, decimals, empty strings, numbers outside the 1 to 100 range. We need to handle every one of those cases without the game crashing.
📄 step2_input.py: Robust input function
def get_guess(attempt, max_attempts):
"""Get a valid integer guess between 1 and 100."""
while True:
raw = input(f"Attempt {attempt}/{max_attempts}, your guess: ")
try:
guess = int(raw)
except ValueError:
print("⚠️ Please enter a whole number.")
continue
if guess < 1 or guess > 100:
print("⚠️ Pick a number between 1 and 100.")
continue
return guess
# Test it
guess = get_guess(1, 7)
print(f"You guessed: {guess}")
▶ Sample interaction
Attempt 1/7, your guess: abc ⚠️ Please enter a whole number. Attempt 1/7, your guess: 150 ⚠️ Pick a number between 1 and 100. Attempt 1/7, your guess: 42 You guessed: 42
What happened here: the while True loop keeps asking until it finally gets something usable. The try/except ValueError catches the case where the player types letters instead of a number, so int("abc") does not crash the program. The range check then throws out anything outside 1 to 100. Only when the input clears every check does return hand the guess back and break the loop. It is like a bouncer at a door: nobody gets through until they meet all the rules. You will reach for this loop-until-valid pattern in just about every interactive program you build.
Step 3: The Game Loop
📄 step3_loop.py: Core game logic
import random
def get_guess(attempt, max_attempts):
while True:
raw = input(f"Attempt {attempt}/{max_attempts}, your guess: ")
try:
guess = int(raw)
except ValueError:
print("⚠️ Please enter a whole number.")
continue
if guess < 1 or guess > 100:
print("⚠️ Pick a number between 1 and 100.")
continue
return guess
def play_round():
secret = random.randint(1, 100)
max_attempts = 7
print("\n🎯 NUMBER GUESSING GAME")
print("I'm thinking of a number between 1 and 100.")
print(f"You have {max_attempts} attempts. Let's go!\n")
for attempt in range(1, max_attempts + 1):
guess = get_guess(attempt, max_attempts)
if guess == secret:
print(f"🎉 CORRECT! You got it in {attempt} attempts!")
return attempt
elif guess < secret:
print("📉 Too low! Try higher.\n")
else:
print("📈 Too high! Try lower.\n")
print(f"💀 Out of attempts! The number was {secret}.")
return None
result = play_round()
What happened here: think of a carnival stall that hands you 7 rings: each toss is one turn, and the round ends the moment you hit the target or run out of rings. play_round() works the same way. It generates the secret, then loops through attempts. Each iteration gets a guess and compares it. The function returns the number of attempts on success, or None on failure. Using for attempt in range(1, max_attempts + 1) gives us automatic attempt counting and a clean exit when attempts run out.
Step 4: Hints and Feedback
Let’s make the hints smarter. When the player gets close, tell them they’re warm.
📄 step4_hints.py: Temperature-based hints
def give_hint(guess, secret):
diff = abs(guess - secret)
direction = "higher" if guess < secret else "lower"
if diff <= 3:
print(f"🔥 SO CLOSE! Just a little {direction}.\n")
elif diff <= 10:
print(f"♨️ Warm! Try {direction}.\n")
elif diff <= 25:
print(f"🌡️ Getting there. Try {direction}.\n")
else:
if guess < secret:
print("📉 Too low! Try higher.\n")
else:
print("📈 Too high! Try lower.\n")
# Test
give_hint(58, 60)
give_hint(30, 60)
give_hint(85, 60)
▶ Output
🔥 SO CLOSE! Just a little higher. 📉 Too low! Try higher. 🌡️ Getting there. Try lower.
What happened here: abs(guess - secret) measures how far off the guess is, no matter which side it lands on. The smaller that gap, the warmer the message. A gap of 3 or less is “so close”, up to 10 is “warm”, up to 25 is “getting there”, and anything bigger falls back to a plain too low or too high. This is the same “hotter or colder” idea from the start of the post, now written in code. Notice that give_hint(85, 60) has a gap of exactly 25, so it lands in the “getting there” band, not the plain “too high” one.
Step 5: Play Again and Scoreboard
📄 step5_replay.py: Replay loop and best score tracking
def play_again():
while True:
answer = input("Play again? (yes/no): ").strip().lower()
if answer in ("yes", "y"):
return True
if answer in ("no", "n"):
return False
print("Please type 'yes' or 'no'.")
def main():
player = input("Enter your name: ").strip() or "Player"
scores = []
while True:
result = play_round()
if result is not None:
scores.append(result)
print("\n🏆 SCOREBOARD")
print(f" {player}: {result} attempts ⭐")
if len(scores) > 1:
print(f" Best: {min(scores)} | Average: {sum(scores)/len(scores):.1f}")
if not play_again():
break
if scores:
print(f"\nThanks for playing, {player}! Final best: {min(scores)} attempts.")
else:
print(f"\nBetter luck next time, {player}!")
main()
What happened here: this step turns one round into an arcade machine. When your run ends, an arcade cabinet asks “Continue?” and keeps the high-score board glowing at the top, and that is exactly what we built. play_again() keeps asking until it gets a clear answer, and .strip().lower() cleans up stray spaces and capitals so “ YES ” still counts as yes. In main(), the or "Player" trick supplies a fallback name when someone just presses Enter, and the scores list remembers every winning attempt count so min(scores) can report the best round. Note that this snippet reuses play_round() from Step 3, so it will not run on its own; the complete program below has everything in one file.
The Complete Game
📄 guessing_game.py: The full program
import random
def get_guess(attempt, max_attempts):
"""Get a valid integer guess between 1 and 100."""
while True:
raw = input(f"Attempt {attempt}/{max_attempts}, your guess: ")
try:
guess = int(raw)
except ValueError:
print("⚠️ Please enter a whole number.")
continue
if guess < 1 or guess > 100:
print("⚠️ Pick a number between 1 and 100.")
continue
return guess
def give_hint(guess, secret):
"""Print a temperature-based hint."""
diff = abs(guess - secret)
direction = "higher" if guess < secret else "lower"
if diff <= 3:
print(f"🔥 SO CLOSE! Just a little {direction}.\n")
elif diff <= 10:
print(f"♨️ Warm! Try {direction}.\n")
elif diff <= 25:
print(f"🌡️ Getting there. Try {direction}.\n")
else:
symbol = "📉" if guess < secret else "📈"
print(f"{symbol} Too {'low' if guess < secret else 'high'}! Try {direction}.\n")
def play_round():
"""Play one round. Returns attempt count on win, None on loss."""
secret = random.randint(1, 100)
max_attempts = 7
print("\n🎯 NUMBER GUESSING GAME")
print("I'm thinking of a number between 1 and 100.")
print(f"You have {max_attempts} attempts. Let's go!\n")
for attempt in range(1, max_attempts + 1):
guess = get_guess(attempt, max_attempts)
if guess == secret:
print(f"🎉 CORRECT! You got it in {attempt} attempt{'s' if attempt > 1 else ''}!")
return attempt
give_hint(guess, secret)
print(f"💀 Out of attempts! The number was {secret}.")
return None
def play_again():
"""Ask player if they want another round."""
while True:
answer = input("Play again? (yes/no): ").strip().lower()
if answer in ("yes", "y"):
return True
if answer in ("no", "n"):
return False
print("Please type 'yes' or 'no'.")
def main():
"""Main game loop with scoreboard."""
print("=" * 40)
print(" 🎮 WELCOME TO THE GUESSING GAME!")
print("=" * 40)
player = input("Enter your name: ").strip() or "Player"
scores = []
while True:
result = play_round()
if result is not None:
scores.append(result)
print(f"\n🏆 SCOREBOARD for {player}")
print(f" This round: {result} attempts")
if len(scores) > 1:
print(f" Games played: {len(scores)}")
print(f" Best: {min(scores)} | Worst: {max(scores)} | Avg: {sum(scores)/len(scores):.1f}")
print()
if not play_again():
break
print()
if scores:
print(f"Thanks for playing, {player}!")
print(f"Games: {len(scores)} | Best: {min(scores)} attempts")
else:
print(f"Better luck next time, {player}!")
if __name__ == "__main__":
main()
What happened here: this is every earlier step stitched into one program. Save it as guessing_game.py and run it with py guessing_game.py (or python3 guessing_game.py on macOS and Linux). Each piece has one job: get_guess collects valid input, give_hint says how warm you are, play_round runs a single game, play_again asks whether to keep going, and main ties them together and keeps the scoreboard. Splitting the work into small functions like this is what stops a 90-line program from turning into spaghetti. The if __name__ == "__main__": guard at the bottom means the game only starts when you run the file directly, not when some other file imports it.
What Could Go Wrong
- “ModuleNotFoundError: No module named ’random’”: this should not happen, because
randomships with Python. If you do see it, check that you did not name your own filerandom.py. That would hide the real built-in module. - Game never ends: if the
while Trueloop inget_guessnever stops, check thatreturn guesssits inside the loop and is indented correctly. - Emojis look broken: some older terminals cannot draw emoji. Swap them for plain text arrows if you need to, like
"^ Too low"and"v Too high". - KeyboardInterrupt when pressing Ctrl+C: that is the normal way to quit. If you want a clean goodbye instead, wrap
main()in atry/except KeyboardInterrupt.
Extend It
The base game works. Now push yourself. Pick one or two of these and work out the implementation on your own:
- Difficulty levels: Easy (1 to 50, 10 attempts), Medium (1 to 100, 7 attempts), Hard (1 to 200, 7 attempts)
- High score persistence: save the best score to a file so it survives between program runs
- Two-player mode: one player picks the number and a friend, say Anvi, has to guess it (clear the screen between turns so she cannot peek)
- Binary search hint: after the game, show the player the best strategy and the fewest guesses it would have taken
Practice Exercises
- Exercise 1: Rebuild the game from memory with a random number between 1 and 50. No peeking at the code above.
- Exercise 2: Add difficulty levels: Easy (1-50), Medium (1-100, 10 guesses), Hard (1-500, 8 guesses).
- Exercise 3: Add scoring by attempts and time taken, then show a top-5 leaderboard after each round.
Conclusion
Your number guessing game is now a real, interactive program. It handles bad input, gives smart feedback, tracks scores across many rounds, and uses functions to keep the code tidy. Every idea from the first 20 posts shows up here: loops, conditionals, input validation, f-strings, lists, and imports. That is not a throwaway exercise. That is software.
Next up: Tuples, Python’s immutable sequences for data that should not change. And if you want the full roadmap from basics to AI/ML, browse every post at the Python + AI/ML tutorial series home.
Frequently Asked Questions
How do I generate a random number in Python?
Use import random then random.randint(a, b) to get a random integer between a and b, inclusive. For example, random.randint(1, 100) returns a number from 1 to 100.
What does if __name__ == ‘__main__’ mean?
It checks whether the file is being run directly (not imported). Code inside this block only runs when you execute the file with python filename.py. If another file imports this module, the block is skipped. It’s a standard Python pattern for making files both importable and runnable.
How do I handle invalid user input in Python?
Wrap int(input()) in a try/except ValueError block. If the user types something that isn’t a number, the except block runs instead of crashing. Combine with a while True loop to keep asking until valid input is received.
Why does the game use 7 attempts for 1-100?
Seven is the maximum number of guesses needed with optimal binary search strategy (2^7 = 128 > 100). This keeps the number guessing game challenging but always winnable if you play smart: cut the range in half with each guess.
How can I make this game harder?
Increase the range (1-500), decrease attempts, add a timer, or remove the too high/too low hints and only say ‘wrong’. You could also add penalty scoring based on how far off each guess was.
Interview Questions on the Number Guessing Game
If you can walk through these without peeking, you are ready for this topic in an interview.
Q: While debugging, you added random.seed(42) at the top of the file. Now the game picks the same secret number every single run. What is happening?
Python’s random module is a pseudo-random generator: it produces numbers from a starting state called the seed. Fixing the seed with random.seed(42) makes the whole sequence repeat exactly, which is great for testing but terrible for gameplay. Remove the seed line and Python will seed itself from the system clock (or OS entropy), so each run gets a different secret. Keeping seeded runs for tests and unseeded runs for players is a common real-world pattern.
Q: What is the difference between random.randint(1, 100) and random.randrange(1, 100)?
random.randint(1, 100) includes both endpoints, so it can return any integer from 1 to 100. random.randrange(1, 100) follows the same convention as range() and excludes the stop value, so it only returns 1 to 99. Mixing them up is a classic off-by-one bug: your game would claim a number between 1 and 100 but never actually pick 100.
Q: A player types 50.5 and the game says “Please enter a whole number” even though 50.5 is a number. Why does that happen, and how would you accept decimals?
int("50.5") raises a ValueError because int() only parses strings that look like whole numbers; it does not truncate decimal strings. So the input falls into the except ValueError branch and the game re-prompts. If you wanted to accept decimals, you could parse with float(raw) first and then convert with round(), but for this game rejecting decimals is the right call since the secret is always a whole number.
Q: In main(), why do we check if result is not None instead of just if result?
play_round() returns the attempt count on a win and None on a loss, so the check must distinguish “lost” from “won”. is not None states that intent explicitly and is the idiomatic Python way to test for None. A plain truthiness check happens to work here because attempts start at 1, but it would silently break if a return value of 0 ever became valid, and an interviewer will expect you to know that difference.
Q: What does .strip().lower() do in play_again(), and what breaks without it?
.strip() removes leading and trailing whitespace and .lower() converts the text to lowercase, so inputs like " YES " or "Y" normalize to "yes" and "y". Without it, the comparison answer in ("yes", "y") would fail for perfectly reasonable answers and the game would nag the player to retype. Normalizing user input before comparing it is a habit worth building early.
Q: Would you use the random module to generate a password reset token or an OTP? Why or why not?
No. The random module is a pseudo-random generator (Mersenne Twister) whose output can be predicted once an attacker observes enough values, so it is fine for games and simulations but unsafe for anything security related. Python ships the secrets module for that job: secrets.randbelow(100) + 1 or secrets.token_urlsafe() draw from a cryptographically secure source. The docs for random state this warning explicitly.
Reference: the complete, always-current details live in the official Python documentation.
Related Posts
Previous: Python List Comprehension: One-Liners That Replace Loops
Next: Python: Tuples, Immutability, Packing, Unpacking, Named
Series Home: Python + AI/ML Tutorial Series

No comment