CHAPTER 20 · BEGINNER4 min read

Project: A number guessing game

Turn loops, conditions, functions, and input validation into a small game with testable rules.

Python 3.12+Free to readModule 4 of 13
YOUR NEXT SMALL WINS

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.

game_rules.pyPython
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]))
Outputstdout
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.

input_check.pyPython
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))
Outputstdout
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.

rule_tests.pyPython
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")
Outputstdout
rules pass
LET IT CLICK

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.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

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.

01

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.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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 False

The loop owns the attempt limit, while evaluate owns the comparison. Invalid input skips the attempt without comparing None to the secret.

02

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.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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 None

Boundary tests catch off-by-one mistakes that a middle-of-the-range example can miss. Invalid input tests document the contract of read_guess.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Why should evaluate be separate from the input loop?
02What does int('abc') raise?
03Which module is appropriate for generating a security token?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

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.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

MODULE 04 · PRACTICAL SELF-CHECK

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 module

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.