RizTech Academy logo
RizTech Academy
Control FlowLesson 6 of 630 min

Practice: building a number-guessing game

Time to build something you can actually play. Everything in it you already know — this lesson is about assembling the pieces rather than learning new ones, which is a different skill and the one that matters.

Build it up step by step alongside this page. Run it after each step. Do not skip ahead to the finished listing at the bottom; assembling it yourself is the entire point.

What we are building

The computer picks a number between 1 and 100. You guess. It tells you higher or lower. You have a limited number of attempts, bad input does not crash it, and you can play again.

Step 1: a secret number

Python's random module can pick one for us:

import random

secret = random.randint(1, 100)
print(secret)

import pulls in code that ships with Python — there is a whole module on this later. randint(1, 100) gives a whole number from 1 to 100, and unusually for Python, both ends are included.

Printing the secret is temporary. It is the simplest possible debugging technique and completely legitimate: you cannot test a guessing game you cannot see the answer to. We remove it at the end.

Step 2: one guess

import random

secret = random.randint(1, 100)
print(f"(debug: the answer is {secret})")

guess = int(input("Guess a number between 1 and 100: "))

if guess == secret:
    print("Correct!")
elif guess < secret:
    print("Too low.")
else:
    print("Too high.")

Run it a few times. It works, and it is not yet a game — you get one guess.

Step 3: keep guessing

We do not know how many guesses it will take, which is exactly the situation while is for:

import random

secret = random.randint(1, 100)
guess = 0

while guess != secret:
    guess = int(input("Guess: "))

    if guess < secret:
        print("Too low.")
    elif guess > secret:
        print("Too high.")

print("Correct!")

Two things worth noticing.

guess = 0 before the loop exists purely so the condition has something to check on the first pass. Since randint(1, 100) can never be 0, the loop always runs at least once.

The "Correct!" moved after the loop. When the condition fails, the guess must have been right, so that is the natural place for it — and it means the if chain only handles the two wrong cases.

Step 4: count the attempts

A guessing game with unlimited guesses is not much of a game.

import random

secret = random.randint(1, 100)
attempts = 0
MAX_ATTEMPTS = 7

while attempts < MAX_ATTEMPTS:
    guess = int(input(f"Guess ({MAX_ATTEMPTS - attempts} left): "))
    attempts += 1

    if guess == secret:
        print(f"Correct! You got it in {attempts} attempts.")
        break
    elif guess < secret:
        print("Too low.")
    else:
        print("Too high.")
else:
    print(f"Out of attempts. The number was {secret}.")

This is the for/else idea from the last lesson, on a while: the else runs only if the loop finished without a break. Running out of attempts means no break, so the "out of attempts" message fires. Guessing correctly breaks, and the else is skipped.

Seven attempts is not arbitrary. Halving the range each time — guess 50, then 25 or 75, and so on — gets you to any number in 1–100 within seven guesses. The game is winnable every time if you play it well, which is what makes it worth playing.

Step 5: stop it crashing

Type abc right now and the program dies:

ValueError: invalid literal for int() with base 10: 'abc'

A program that crashes on a typo is not finished. From the type conversion lesson, check before converting:

    raw = input(f"Guess ({MAX_ATTEMPTS - attempts} left): ").strip()

    if not raw.isdigit():
        print("Please enter a whole number.")
        continue

    guess = int(raw)

continue skips the rest of the pass and asks again. Critically, it happens before attempts += 1, so a typo does not cost the player a turn. That is a deliberate decision — it would be equally valid to make typos cost an attempt, but silently punishing someone for a slip is poor design.

Worth being honest: isdigit() returns False for -5, so negative numbers are rejected as invalid rather than as out of range. Acceptable here, since the range is 1 to 100. try/except in module 6 handles this properly.

Step 6: reject out-of-range guesses

150 is a number, but not a useful one:

    if guess < 1 or guess > 100:
        print("Out of range. Pick between 1 and 100.")
        continue

Or, using chaining:

    if not 1 <= guess <= 100:
        print("Out of range. Pick between 1 and 100.")
        continue

Again before the attempt counter increments.

Step 7: play again

Wrap the whole game in an outer loop:

while True:
    play_one_game()

    again = input("\nPlay again? (y/n): ").strip().lower()
    if again != "y":
        print("Thanks for playing.")
        break

We do not have functions yet, so for now the game body goes directly inside. That nesting is slightly uncomfortable, and that discomfort is the point — it is exactly the problem functions solve in module 5.

Note .strip().lower() on the answer, so Y, y and y all work. Anything other than y ends the game, which is more forgiving than demanding exactly n.

The finished program

import random

MAX_ATTEMPTS = 7
LOWEST = 1
HIGHEST = 100

print("Guess the number!")
print(f"I am thinking of a number between {LOWEST} and {HIGHEST}.")

playing = True

while playing:
    secret = random.randint(LOWEST, HIGHEST)
    attempts = 0

    while attempts < MAX_ATTEMPTS:
        remaining = MAX_ATTEMPTS - attempts
        raw = input(f"\nGuess ({remaining} left): ").strip()

        if not raw.isdigit():
            print("Please enter a whole number.")
            continue

        guess = int(raw)

        if not LOWEST <= guess <= HIGHEST:
            print(f"Out of range. Pick between {LOWEST} and {HIGHEST}.")
            continue

        attempts += 1

        if guess == secret:
            print(f"Correct! You got it in {attempts} attempts.")
            break
        elif guess < secret:
            print("Too low.")
        else:
            print("Too high.")
    else:
        print(f"\nOut of attempts. The number was {secret}.")

    again = input("\nPlay again? (y/n): ").strip().lower()
    if again != "y":
        print("Thanks for playing.")
        playing = False

Around forty lines, and it uses almost everything from the first three modules: variables, constants, f-strings, type conversion, comparison, if/elif/else, two kinds of loop, break, continue, and while/else.

What to notice

The constants at the top. Changing the range means editing three values in one place rather than hunting through the code. MAX_ATTEMPTS in capitals says "this is a setting, not a working variable".

Validation comes before counting. Everything that rejects input uses continue and sits above attempts += 1. Order is doing real work here.

The nesting is the weak point. Two loops deep with the whole game inside is close to the limit of what reads comfortably. Module 5 fixes it properly.

The debug print is gone. It did its job and was removed.

Practice

  1. Build it up step by step yourself. Run it after every step.
  2. Test it deliberately: abc, empty input, 0, 101, -5, 50.5. Which are handled well, which give a confusing message? 50.5 is interesting — work out why it is rejected.
  3. Change the range to 1–1000. How many attempts should you allow to keep it fair? Work it out rather than guessing — roughly how many times can you halve 1000 before reaching 1?
  4. Add a difficulty choice at the start: easy is 1–50 with 8 attempts, hard is 1–100 with 5.
  5. Track the best score across games in one session and print it at the end.
  6. After a wrong guess, tell the player if they are within 5 of the answer — "Very close!". You will need abs().
  7. Reverse it. You think of a number and the computer guesses, using higher or lower feedback from you. Make it guess intelligently by halving the remaining range each time. This is binary search, one of the genuinely important algorithms, and you now know everything you need to write it.

Question 7 is the hardest thing in this module and worth the time. If you get stuck, write down by hand how you would narrow 1–100 to a single number, then turn each step into code.


That is module three. Your programs can now decide and repeat, which is the point at which they stop being calculators.

Next module: data structures — lists, dictionaries and the rest, which is where you start handling real amounts of information rather than one value at a time.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship