~90 minutes. Take your Lab 2 guess-the-number game and refactor it into named functions with docstrings. The script's "main" code should be ~6 lines that read like an outline.
Goal: rewrite Lab 2's lab-2-guess.py so that the same behavior is implemented as a main() function calling three smaller named functions. Each function has a one-line docstring.
Estimated time: 90 minutes
Prerequisites: Lab 2 complete and committed. Week 3 lecture (functions, scope, docstrings).
Setup
mkdir -p ~/fnd-102/lab-3
cd ~/fnd-102/lab-3
cp ../lab-2/lab-2-guess.py lab-3-guess.py
Open lab-3-guess.py.
Part A: Identify the three functions (15 min)
Read your Lab 2 code. Notice that it does three distinct things:
- Setup. Pick a random secret number; print the welcome message; initialize the try counter.
- The guess loop. Repeatedly ask the player for a guess; compare to the secret; respond; track tries.
- The summary. After the loop, decide whether the player won or lost; print the result; choose the exit code.
Each of these is a function candidate. Sketch the three functions on paper before writing code:
pick_secret_number()-> intplay_guess_loop(secret, max_tries)-> bool (True if the player won, False if they lost)summarize_and_exit(won, secret, max_tries)-> exits the program
The main() function then orchestrates:
def main():
print('Welcome to guess-the-number!')
secret = pick_secret_number()
won = play_guess_loop(secret, max_tries=7)
summarize_and_exit(won, secret, max_tries=7)
if __name__ == '__main__':
main()
The if __name__ == '__main__': idiom is new. It means "run main() when this file is executed directly, but not when it is imported as a module." You will use this pattern in every CLI tool from now on. The official Python docs explain the mechanism at https://docs.python.org/3/library/__main__.html.
Part B: Write the three functions (45 min)
For each function, write the signature, the docstring, and the body. Use this structure:
def pick_secret_number():
"""Return a random integer from 1 through 100 inclusive."""
return random.randint(1, 100)
def play_guess_loop(secret, max_tries):
"""Play the guess loop. Return True if the player guessed correctly within max_tries, False otherwise."""
tries_left = max_tries
while tries_left > 0:
try:
guess = int(input(f'Your guess ({tries_left} tries left): '))
except ValueError:
print('Please type a number.')
continue
if guess < 1 or guess > 100:
print('Out of range; try 1-100.')
continue
if guess == secret:
return True
if guess < secret:
print('Too low.')
else:
print('Too high.')
tries_left -= 1
return False
def summarize_and_exit(won, secret, max_tries):
"""Print the win or lose summary and exit with status 0 (win) or 1 (loss)."""
if won:
print('You got it!')
sys.exit(0)
else:
print(f'You ran out of tries. The number was {secret}.')
sys.exit(1)
Notice three things:
- Each function has ONE job.
pick_secret_numberpicks a number. It does not print. It does not loop. If you find yourself writing a function that does several unrelated things, split it. - Docstrings are imperative voice. "Return..." not "Returns...". "Play..." not "Plays...". PEP 257 codifies this; the consistency makes a codebase scan faster.
- Return values are explicit.
play_guess_loopreturns True or False;pick_secret_numberreturns an int. The function's signature + docstring tell you everything you need to know to call it without reading the body.
Part C: The main() function (10 min)
Write main() as a six-line orchestrator:
def main():
print('Welcome to guess-the-number!')
max_tries = 7
secret = pick_secret_number()
won = play_guess_loop(secret, max_tries)
summarize_and_exit(won, secret, max_tries)
if __name__ == '__main__':
main()
The main() function reads like an outline of what the program does. A reader who lands on this file for the first time can understand the game's structure in 10 seconds without reading the function bodies.
Part D: Test by reading help() (10 min)
In a Python REPL from your ~/fnd-102/lab-3 directory:
>>> import lab_3_guess
# Note: hyphens are not allowed in Python module names; rename the file to lab_3_guess.py first
# (or just open the REPL inside the file's directory and use `from lab_3_guess import ...`)
>>> help(lab_3_guess.pick_secret_number)
>>> help(lab_3_guess.play_guess_loop)
>>> help(lab_3_guess.summarize_and_exit)
Each should print the function's docstring as the help output. If help() prints nothing useful, your docstring is missing or in the wrong place (it must be the first statement in the function body).
Part E: Commit your work (10 min)
cd ~/fnd-102/lab-3
git add lab-3-guess.py # or lab_3_guess.py if you renamed
git commit -m "lab-3: refactor guess-the-number into pick / play / summarize functions with docstrings"
Optionally, make a second commit that improves a docstring or extracts one more helper. The Lab 10 PR submission expects multi-commit history; practicing here costs nothing.
Expected output / artifact
lab-3-guess.py (or lab_3_guess.py) should:
- Behave identically to Lab 2 (same game, same exit codes, same UX polish)
- Have three named functions:
pick_secret_number,play_guess_loop,summarize_and_exit - Have a
main()function of ~6 lines - Have the
if __name__ == '__main__':block at the bottom - Every function has a one-line imperative-voice docstring
help(function_name)in the REPL shows the docstring for each
The file is committed to your Git repo at ~/fnd-102/lab-3/lab-3-guess.py.
What's the failure mode?
This refactor's likely failure modes:
- You break the game while refactoring. The most common refactoring bug: the new code does not do what the old code did. Always run the new version through one full win and one full loss before committing. If the game now lets you guess forever, you broke the try-counter wiring during the move into
play_guess_loop. - You accidentally make a function do two things.
pick_secret_number()that also prints "Welcome to guess-the-number!" is a function with two jobs. Welcome belongs inmain(). If you find yourself naming a function withandin the name (pick_secret_and_print_welcome), split it. - You over-extract. A function
print_too_high()that contains onlyprint('Too high.')is a function called from one place that adds nothing. Inline calls of one line do not need extraction.
Common pitfalls
- Forgetting
if __name__ == '__main__':. Without it, yourmain()runs as a side effect ofimporting the module. The result: anyone who imports your file (the test suite in week 13 will) accidentally starts a game. - Mutable state in a function. If
play_guess_loopmodifies a globaltries_left, you have leaked state out of the function. Use parameters and return values; do not touch globals. returninsideif/elif/elsethat has noelse. A function withif x > 0: return ...but no fallback returnsNonefor the missing case. Either add anelseor make sure the function genuinely should returnNonethere.- Naming the file with hyphens.
lab-3-guess.pyis fine to RUN (python3 lab-3-guess.py) but cannot be IMPORTED (import lab-3-guessis a syntax error; hyphens become minus signs). Use underscores for module names:lab_3_guess.py. This rule applies to every Python file you may want to import. Rename Lab 1 + Lab 2 retroactively if you plan to test them in week 13.
Stretch (optional)
-
Add a fourth function:
read_max_tries_from_args()that returns the max-tries value parsed from the command line (default 7). Usesys.argvfor now; week 6 introduces argparse and you will rewrite this then. -
Add an optional
seedparameter topick_secret_number(seed=None)that, if provided, callsrandom.seed(seed)before generating. This makes the function testable: withseed=42, the same number always comes back. Forward-pointer to week 13's pytest. -
Extract one more function from the body of
play_guess_loop. Candidates:prompt_for_guess(tries_left) -> int | None(returns the parsed guess orNoneon invalid input);compare_guess_to_secret(guess, secret) -> str(returns "too_high", "too_low", or "correct"). Both improve testability; both also risk over-extraction. Pick one and try it. Notice the trade-off.
Lab 3 v0.1.