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 game serves as a perfect laboratory for identifying common code smells—like magic numbers, unnecessary recursion, and bloated class states. Refactoring this project isn't just about making the game run; it is about decoupling logic from data and ensuring each function has a single, clear responsibility. By cleaning up these structural issues, we transform a fragile script into a robust application.
Prerequisites
To follow this tutorial, you should have a firm grasp of fundamentals, including classes, methods, and list indexing. You should also understand how to run scripts from the command line and be familiar with basic control flow structures like 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 . Any CLI tool requiring user validation—such as a bank account manager or a configuration wizard—benefits from a centralized, validated input utility. Decoupling the "checking" logic (the callback) from the "fetching" logic (the input loop) allows you to reuse the same input UI for different validation rules across your entire codebase.
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 lists 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.
- 40%· games
- 40%· programming languages
- 20%· people

Refactoring a Battleship Game in Python // Code Roast Part 1 of 2
WatchArjanCodes // 24:38
On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!