Clean Code: Refactoring User Input and State Management in Python

ArjanCodes////4 min read

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 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.
Topic DensityMention share of the most discussed topics · 5 mentions across 3 distinct topics
40%· games
40%· programming languages
20%· people
End of Article
Source video
Clean Code: Refactoring User Input and State Management in Python

Refactoring a Battleship Game in Python // Code Roast Part 1 of 2

Watch

ArjanCodes // 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!

What they talk about
AI and Agentic Coding News
Who and what they mention most
Python
33.3%5
Python
20.0%3
Python
20.0%3
Pydantic
13.3%2
4 min read0%
4 min read