Clean Code: Refactoring User Input and State Management in Python
Overview: The Cost of Poor Design
Writing code that works is only half the battle. In software development, the real challenge lies in writing code that is maintainable, readable, and resilient to change. A simplified
Prerequisites
To follow this tutorial, you should have a firm grasp of if/else statements and loops.
Key Libraries & Tools

- Python 3.x: The core programming language used for the implementation.
- Random Module: Used for generating ship locations on the grid.
- OS Module: Utilized for clearing the terminal screen to maintain a clean user interface during gameplay.
- Typing Module: Specifically
Callable, to define clear interfaces for callback functions during refactoring.
Refactoring the User Input Mechanism
One of the most frequent mistakes in beginner code is mixing input logic with game logic. The original code used recursion to handle invalid inputs, which can lead to stack overflow errors if a user is particularly stubborn. Replacing this with a while True loop and a try-except block makes the input much more robust.
def read_int(prompt: str, min_value: int = 1, max_value: int = 5) -> int:
while True:
try:
line = input(prompt)
value = int(line)
if value < min_value:
print(f"The minimum value is {min_value}. Try again.")
continue
if value > max_value:
print(f"The maximum value is {max_value}. Try again.")
continue
return value
except ValueError:
print("That's not a number. Try again.")
This generic read_int function is a workhorse. It handles the conversion, validates ranges, and catches non-numeric strings without crashing the program. By defining parameters for min_value and max_value, we eliminate the need to hardcode board boundaries inside our input functions.
Decoupling Logic from the Game Class
The original Game class suffered from "state bloat," storing temporary variables like the current guess as instance variables. This is a mistake. If a value is only needed for a single calculation or turn, it should be a local variable. We also extracted the read_guess logic into a standalone function that takes a Callable as an argument. This allows the function to check if a coordinate was already guessed without needing to know the internal structure of the Board object.
def read_guess(already_guessed: Callable[[int, int], bool]) -> tuple[int, int]:
while True:
row = read_int("Guess the row: ") - 1
col = read_int("Guess the column: ") - 1
if not already_guessed(row, col):
return row, col
print("You already guessed that! Try again.")
Syntax Notes
- f-strings: We use f-strings (e.g.,
f"Value: {val}") for cleaner, more readable string interpolation compared to the older%or.format()methods. - Type Hinting: By using
Callable[[int, int], bool], we explicitly define that a function must accept two integers and return a boolean. This makes the code self-documenting. - Removing
objectInheritance: In Python 3, classes inherit fromobjectby default. Writingclass Game(object):is redundant and should be simplified toclass Game:.
Practical Examples
This refactoring pattern applies far beyond
Tips & Gotchas
- Avoid recursion for UI loops: Use
whileloops for user input. Recursion is for recursive data structures, not for waiting on a human to type the right number. - Handle Off-by-One Errors: Remember that users think in 1-based indexing, while Pythonlists use 0-based indexing. Always subtract 1 from user input immediately after validation.
- Keep Comments Professional: Avoid putting opinions or "easy to read" claims in comments. Code should be clear enough that the comments only explain the why, not the what.

Fancy watching it?
Watch the full video and context