Project: A number guessing game
Turn loops, conditions, functions, and input validation into a small game with testable rules.
By the end, you’ll be able to…
- Separate the game rules from the input and output loop.
- Validate text before converting it to a number, and keep attempts bounded.
- Test low, high, win, and invalid-input paths without playing interactively.
01Write the contract before the code
The game chooses a whole number from 1 to 100. A player has seven attempts. After each guess the game reports whether the guess is too low, too high, or correct. The player wins when the secret is found; otherwise the game reveals the secret after the final attempt. Write those rules down before deciding how the loop will look.
The most important design choice is to separate the rule from the interface. evaluate(secret, guess) can compare two numbers and return a result. The input loop can handle prompts, attempts, and messages. That separation makes the core rule testable without a keyboard, and it lets you change the interface later without rewriting the game logic.
02Keep the loop bounded
A for loop over range(1, attempts + 1) already states the maximum number of guesses. enumerate is useful when the attempt number belongs in the message. Return True as soon as the guess is correct, and return False only after the loop has exhausted its attempts. This keeps the stopping condition visible in one place.
The example uses a fixed secret so its output is predictable. In the real game, random.randint(1, 100) creates a secret for each round. random is appropriate for a game, but it is not suitable for passwords, tokens, or anything security-sensitive; the secrets module is designed for those values.
def evaluate(secret, guess): if guess < secret: return "low" if guess > secret: return "high" return "win"def play(secret, guesses): for attempt, guess in enumerate(guesses, start=1): result = evaluate(secret, guess) print(f"Attempt {attempt}: {result}") if result == "win": return True return Falseprint(play(42, [50, 25, 42]))Attempt 1: high Attempt 2: low Attempt 3: win True
03Validate input before converting
input returns text. int(text) raises ValueError when the text is not a valid whole number, and a valid integer can still be outside the allowed range. A small read_guess function can convert the text, catch the conversion failure, and return None for any invalid guess. The caller can then print one consistent message instead of wrapping every prompt in its own try block.
Returning None is a deliberate sentinel here: None cannot be a valid guess because every valid guess is an integer from 1 to 100. Make sure the caller checks for it before comparing. Avoid catching every exception; catch ValueError around the conversion that can actually raise it.
def read_guess(text): try: value = int(text) except ValueError: return None if 1 <= value <= 100: return value return Nonefor raw in ("50", "abc", "0", "100"): print(raw, "->", read_guess(raw))50 -> 50 abc -> None 0 -> None 100 -> 100
04Test the rules without playing
A deterministic test can call evaluate with a known secret and assert each outcome. Test a guess below the secret, above it, and equal to it. Then test the input helper with a valid number, a non-number, zero, and a value above the range. These checks run in milliseconds and do not need a person at the keyboard.
The full game loop can accept read and write functions as parameters, which lets a test replace input and print with small fakes. That is dependency injection at a very small scale. For this project, the rule tests and a manual play-through are enough; the important habit is that the core behavior is not trapped inside an interactive loop.
def evaluate(secret, guess): if guess < secret: return "low" if guess > secret: return "high" return "win"def test_evaluate(): assert evaluate(42, 10) == "low" assert evaluate(42, 90) == "high" assert evaluate(42, 42) == "win"test_evaluate()print("rules pass")rules pass
The ideas worth keeping.
- Separate the rule from the interface so the rule can be tested independently.
- Bound the loop and validate text before comparing it with the secret.
- Test the boundaries and invalid input, not only the happy path.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Complete the game loop
Build a play_game(secret, attempts=7) function that prompts for a guess, validates it, prints low/high/win feedback, and reveals the secret after the final attempt. Keep the rule function separate.
Explore a worked solution
def play_game(secret, attempts=7): for attempt in range(1, attempts + 1): guess = read_guess(input(f"Guess {attempt}/{attempts}: ")) if guess is None: print("Enter a whole number from 1 to 100.") continue result = evaluate(secret, guess) if result == "win": print(f"You won in {attempt} attempts.") return True print("Too low." if result == "low" else "Too high.") print(f"Out of attempts. The secret was {secret}.") return FalseThe loop owns the attempt limit, while evaluate owns the comparison. Invalid input skips the attempt without comparing None to the secret.
Make the rule testable
Write tests for the secret boundaries 1 and 100. Include a test that proves an invalid guess raises the expected error or returns the documented result.
Explore a worked solution
def test_boundaries(): assert evaluate(1, 1) == "win" assert evaluate(100, 100) == "win" assert evaluate(1, 2) == "high" assert evaluate(100, 99) == "low"def test_invalid_input(): assert read_guess("abc") is None assert read_guess("0") is None assert read_guess("101") is NoneBoundary tests catch off-by-one mistakes that a middle-of-the-range example can miss. Invalid input tests document the contract of read_guess.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Complete the game loop
Build a play_game(secret, attempts=7) function that prompts for a guess, validates it, prints low/high/win feedback, and reveals the secret after the final attempt. Keep the rule function separate.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
Pause. Put the pieces together.
You’ve reached the end of Functions and reusable programs. Before moving on, use this task to check what you can do independently.
Separate calculation from display and call it from a second file without running unwanted code on import.
Try it in your own environment and write down the result. This is a self-check; reading completion does not evaluate your code.
Review this moduleOne chapter closer.
Reading is a start. Trying it yourself makes the connection.