Modernizing Chess Logic: A Deep Dive into Python Refactoring and Clean Design
Overview
Building a chess game in is a classic rite of passage for many developers. However, the logic behind moving pieces, validating checks, and managing the board state can quickly spiral into a "big ball of mud." In this session, we analyze a common implementation that suffers from tight coupling, bloated inheritance hierarchies, and violations. By shifting from a complex subclass-per-piece model to a more flexible, data-driven architecture, we can make the codebase significantly easier to maintain and test.
Prerequisites
Before diving in, you should have a solid grasp of in Python. Familiarity with classes, inheritance, and basic development is essential. You should also understand common Python data structures like lists, tuples, and dictionaries, as we use these to simplify the internal representation of the chessboard.

Key Libraries & Tools
- : Python's standard library for creating graphical user interfaces. While simple, it requires careful resource management to avoid memory leaks during frequent screen refreshes.
- : Introduced in , these provide a decorator and functions for automatically adding generated special methods to user-defined classes.
- : A set of symbolic names bound to unique, constant values, used here to replace "magic numbers" for piece colors and types.
- : Part of the
typingmodule, these allow for structural subtyping, which is perfect for creating lightweight interfaces without strict inheritance.
Refactoring the Piece Hierarchy
The original code used a separate subclass for every single piece type—Pawn, Rook, Knight, etc. While this feels intuitive at first, it leads to a mess of isinstance checks whenever the logic needs to identify a piece. Instead of this deep inheritance, we can use a single Piece class driven by enums.
from dataclasses import dataclass
from enum import Enum, auto
class PieceType(Enum):
PAWN = auto()
ROOK = auto()
KNIGHT = auto()
# ... other pieces
class Color(Enum):
WHITE = 0
BLACK = 1
@dataclass
class Piece:
x: int
y: int
color: Color
type: PieceType
def promote_to_queen(self):
self.type = PieceType.QUEEN
By collapsing the hierarchy, we treat piece types as data rather than distinct types. This simplifies operations like promoting a pawn; instead of destroying one object and instantiating another, we simply update an attribute.
Decoupling the Board State
A major issue in many chess implementations is the violation, where the or logic layer reaches three or four levels deep into a board's internal arrays. To solve this, we create a dedicated Board class that wraps the underlying data structure (in this case, a dictionary mapping positions to pieces).
@dataclass
class Board:
pieces: dict[tuple[int, int], Piece] = field(default_factory=empty_board)
def get_piece_at(self, pos: tuple[int, int]) -> Piece | None:
return self.pieces.get(pos)
def is_empty(self, pos: tuple[int, int]) -> bool:
piece = self.get_piece_at(pos)
return piece is None or piece.type == PieceType.EMPTY
This abstraction allows the to ask questions like "is this square empty?" without needing to know if the board is a 2D list, a dictionary, or a bitboard.
Simplifying Move Logic with Guard Clauses
Deeply nested if statements are the enemy of readability. When checking for Checkmate or valid moves, the code often wanders six or seven levels deep. We can flatten this using . Instead of wrapping the entire function in a check, we exit early if conditions aren't met.
# Before refactoring (nested)
def check_for_mate(self, color):
for piece in self.board:
if piece.color == color:
moves = piece.get_moves()
if moves:
return False
return True
# After refactoring (flattened with guard clauses)
def check_for_mate(self, color):
for piece in self.board:
if piece.color != color:
continue
moves = piece.get_moves()
if moves:
return False
return True
Syntax Notes & Best Practices
- Avoid Wildcard Imports: Never use
from tkinter import *. It pollutes your namespace and makes it impossible to track where classes likeFrameorButtonoriginate. Useimport tkinter as tkinstead. - Type Hinting: Be specific. Don't just hint
list; uselist[tuple[int, int]]. This allows tools like or to catch bugs before you even run the code. - Snake Case: Python standardizes on
snake_casefor methods and variables. AvoidcamelCaseto stay consistent with the style guide.
Tips & Gotchas
One common pitfall in is recreating images and buttons on every refresh. In the original code, every move loaded new .png files from the disk and layered new buttons on top of old ones. This creates a massive memory leak and slows the game down over time. Always cache your PhotoImage objects and reuse existing elements by updating their configuration rather than destroying and recreating them. Finally, keep your creation logic (like parsing a ) separate from your runtime logic to keep your classes focused and testable.
- 13%· concepts
- 13%· languages
- 13%· libraries
- 6%· concepts
- 6%· concepts
- Other topics
- 50%

Step-By-Step Chess Game Refactoring | Code Roast
WatchArjanCodes // 32:55
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!