Advanced Python Refactoring: Transforming Conway's Game of Life
The Anatomy of Brittle Python Code
Writing code that works is only the first step. Writing code that lasts requires a deep understanding of structure and maintainability. When reviewing an implementation of
A primary concern in many beginner-to-intermediate projects is the "God File"—a single Python script where classes for logic, data storage, and visualization all live together. This makes navigation a chore and testing nearly impossible. Furthermore, ignoring type hints creates a layer of "imprecise errors." For instance, a method named is_alive should return a boolean, but without annotations, it might return an integer like 0 or 1, leading to subtle logic bugs that are difficult to track down during execution.

Moving from Unit Test to Pytest
unittest module, but
One of the most powerful features to adopt is the pytest fixture. Instead of repetitive setup code inside every test method, you define a fixture to handle the initialization of your grid or game state.
import pytest
from grid import Grid
@pytest.fixture
def basic_grid():
return Grid(rows=3, cols=3)
def test_grid_initialization(basic_grid):
assert basic_grid.rows == 3
assert basic_grid.cols == 3
By passing the fixture as an argument to your test functions, you create a clear, modular safety net. This allows you to refactor your core logic with the confidence that any breaking change will be caught immediately by your 27+ passing tests.
Simplification Through Functional Design
It is a common mistake to over-engineer logic by wrapping every simple behavior in a class. In the original version of this project, each rule for the cellular automaton—such as birth or overpopulation—was its own class with a static method. This is unnecessary overhead.
A rule is, at its heart, a function: it takes inputs (current state and neighbors) and returns an output (new state). By leveraging the latest features in Callable type alias to enforce this pattern strictly.
from typing import Callable, Optional
type Rule = Callable[[int, int], Optional[int]]
def birth_rule(cell: int, neighbors: int) -> Optional[int]:
if cell == 0 and neighbors == 3:
return 1
return None
Replacing class instances with a list of rule functions makes the update loop in the
Decoupling and the Law of Demeter
The Law of Demeter suggests that a module should not reach deep into the internals of another object. The original code violated this by having the main function reach through the Game class into the Grid class and then into a raw list of lists to set a cell's value. This "dot-walking" (e.g., game.grid.grid[0][0] = 1) makes your code extremely brittle.
If you decide to change your internal storage from a list of lists to a Grid class should have a set_cell method and a raw_grid property. This way, the caller doesn't need to know how the grid is stored, only that it can be updated and retrieved.
Implementing Iterators and Protocols
To further clean up the simulation loop, you can turn your for loops with range(rows) and range(cols) inside your game logic, let the grid handle its own iteration. Using the yield keyword, the grid can provide the row, column, and cell value directly to the loop.
def __iter__(self):
for r in range(self.rows):
for c in range(self.cols):
yield r, c, self.grid[r][c]
For visualization, use the Protocol class from the typing module. This allows you to define exactly what a visualizer needs—such as an update method and a raw_grid property—without forcing a strict inheritance hierarchy. This makes it easy to swap between a
Tips and Syntax Best Practices
Modern Python development favors clarity and configuration over hard-coded values. Move your magic numbers and strings—like row counts or sleep times—out of the main function and into constants at the top of your file. This makes the code easier to tune and eventually move to environment variables or a pyproject.toml configuration.
Always place your imports at the top of the file. While "lazy loading" imports inside functions can technically save a few milliseconds of startup time, it hides dependencies and makes the code harder to debug. Use tools like Pylint to catch these style violations early. By combining strict type hinting, decoupled class structures, and a robust functional approach, you transform a fragile script into a professional, maintainable library.

Fancy watching it?
Watch the full video and context