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

Clean Code: Refactoring User Input and State Management in Python
Refactoring a Battleship Game in Python // Code Roast Part 1 of 2
  • 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 object Inheritance: In Python 3, classes inherit from object by default. Writing class Game(object): is redundant and should be simplified to class 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 while loops 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
    Python
    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.
Clean Code: Refactoring User Input and State Management in Python

Fancy watching it?

Watch the full video and context

4 min read