Modernizing Chess Logic: A Deep Dive into Python Refactoring and Clean Design
Overview
Building a chess game in
Prerequisites
Before diving in, you should have a solid grasp of

Key Libraries & Tools
- TKinter: Python's standard library for creating graphical user interfaces. While simple, it requires careful resource management to avoid memory leaks during frequent screen refreshes.
- Data Classes: Introduced inPython, these provide a decorator and functions for automatically adding generated special methods to user-defined classes.
- Enum: A set of symbolic names bound to unique, constant values, used here to replace "magic numbers" for piece colors and types.
- Protocol: 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 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
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
# 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 likeMypyorPyrightto catch bugs before you even run the code. - Snake Case: Python standardizes on
snake_casefor methods and variables. AvoidcamelCaseto stay consistent with thePEP 8style guide.
Tips & Gotchas
One common pitfall in .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

Fancy watching it?
Watch the full video and context