Milestone 1: Python 3.12 Modernization with Type Safety and Testing#3
Merged
mintzer merged 14 commits intoJun 29, 2026
Merged
Conversation
- position.py: convert to @DataClass(frozen=True) with typed row/column fields - colors.py: annotate all color class attributes as tuple[int, int, int]; annotate get_cell_colors() return type - block.py: annotate all instance attributes and method signatures - blocks.py: fix LBlock tab indentation to 4-space; add -> None return types to all __init__ methods - grid.py: annotate all instance attributes and method signatures - game.py: replace wildcard 'from blocks import *' with explicit imports; add Block import; annotate all attributes and methods; replace == False/== True with not/direct bool checks - main.py: split 'import pygame,sys' into two lines; add if __name__ == "__main__": guard wrapping game loop; replace all == True/== False comparisons with idiomatic boolean checks; annotate all top-level variables
… (conftest.py already handles it)
…stic block sequences
…e repeated offset reset in test_block.py
…inside test_get_random_block_removes_from_bag
…locks() factory in game.py and _fresh_bag() helper in test_game.py
…own, rotate, and reset in Game
…ing block subclasses (JBlock, IBlock, OBlock, SBlock, TBlock, ZBlock)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
View Milestone
Table of Contents
Status
Successfully completed. All deliverables specified in the Milestone Plan were implemented:
pyproject.tomlintroduced withrequires-python = ">=3.12",pygame-ce>=2.5.0, and dev extras (mypy,ruff,pytest).mypy --strictenabled inpyproject.toml.E,W,F,UP,B,I; formatting normalized across the codebase.LBlocktab indentation normalized, wildcard import replaced with explicit imports, explicit boolean comparisons removed,if __name__ == "__main__":guard added, comma-joined imports split.tests/directory created withconftest.py,test_grid.py,test_block.py, andtest_game.py(473 lines total).A
[build-system]table was initially missing frompyproject.tomland the build backend was incorrect (hatchlinginstead ofsetuptools); both were corrected in follow-up commits. Post-review commits also improved test quality:random.seed(42)was added toconftest.py, azeroed_lblockfixture was extracted, a_all_blocks()factory was introduced ingame.py, and additional tests forGame.move_left,Game.move_right,Game.move_down,Game.reset, and all block subclass cell layouts were added.No deviations from the Milestone Plan were made.
Feature Overview
This milestone modernizes the Python Tetris Game codebase from an unversioned, untyped, untested Python 3 project to a clean Python 3.12 application. No end-user-visible gameplay changes are introduced; the game plays identically to before. The developer-facing changes are:
pip install -e ".[dev]"installs all runtime and development dependencies frompyproject.toml.mypy --strictpasses with no errors.ruff check .andruff format .enforce PEP 8 compliance, idiomatic Python 3.12 syntax, and consistent import ordering.pytest tests/validates grid boundary checks, row clearing, block transforms, scoring logic, block-bag refill, and core game actions.Testing
Automated Testing
Run the full test suite from the repository root:
Test files and coverage:
tests/test_grid.py—Grid.is_inside,Grid.is_empty,Grid.is_row_full,Grid.clear_row,Grid.move_row_down,Grid.clear_full_rows,Grid.resettests/test_block.py—Block.move,Block.get_cell_positions,Block.rotate,Block.undo_rotation; cell layout assertions for all 7 block subclassestests/test_game.py—Game.update_score(all scoring branches),Game.get_random_block(7-bag refill),Game.move_left,Game.move_right,Game.move_down,Game.rotate,Game.resetType checking:
mypy --strict .Linting:
ruff check .Manual Testing
Architecture
Overview
graph TD subgraph Legend[" "] direction LR LY["Modified"]:::modified LG["New"]:::new end pyproject["pyproject.toml"]:::new main["main.py\n(entry point)"]:::modified game["game.py\n(game state)"]:::modified grid["grid.py\n(board state)"]:::modified block["block.py\n(block base class)"]:::modified blocks["blocks.py\n(7 Tetromino types)"]:::modified position["position.py\n(cell position)"]:::modified colors["colors.py\n(color palette)"]:::modified main --> game game --> grid game --> block blocks --> block block --> position grid --> position game --> colors grid --> colors classDef modified fill:#FFD700,stroke:#333,color:#000 classDef new fill:#90EE90,stroke:#333,color:#000Changes
pyproject.tomlNew file at the repository root. Declares
name = "python-tetris-game-pygame",requires-python = ">=3.12", runtime dependencypygame-ce>=2.5.0,<3.0, and dev extrasmypy>=1.10,ruff>=0.4,pytest>=8.0. Includes[tool.ruff],[tool.ruff.lint],[tool.mypy], and[tool.pytest.ini_options]configuration sections. A[build-system]table (setuptools.build_meta) was added in a follow-up fix commit.position.pyThe
Positionclass was converted from a hand-written__init__-only class to@dataclass(frozen=True)with typed fieldsrow: intandcolumn: int. This eliminates boilerplate, adds__eq__,__repr__, and__hash__automatically, enforces immutability, and enables reliable equality comparisons in tests and game logic.colors.pyAll class-level color constants annotated as
tuple[int, int, int]. Theget_cell_colors()return type annotated aslist[tuple[int, int, int]].block.pyAll instance attributes annotated (
id: int,cells: dict[int, list[Position]],cell_size: int,row_offset: int,column_offset: int,rotation_state: int,colors: list[tuple[int, int, int]]). All method signatures annotated with parameter types and-> None/-> list[Position]/-> boolreturn types.blocks.pyTab indentation in
LBlock.__init__normalized to 4-space (matching all other block subclasses). All__init__methods annotated with-> Nonereturn types.grid.pyThe
gridattribute annotated aslist[list[int]]. All method signatures annotated, includingdraw(screen: pygame.Surface) -> None,is_inside(row: int, column: int) -> bool,is_row_full(row: int) -> bool,clear_row(row: int) -> None, andmove_row_down(row: int, num_rows: int) -> None.game.pyWildcard import
from blocks import *replaced with an explicit import listing all seven block classes (IBlock,JBlock,LBlock,OBlock,SBlock,TBlock,ZBlock). ABlockbase class import was added. All instance attributes annotated (grid: Grid,blocks: list[Block],current_block: Block,next_block: Block,game_over: bool,score: int,rotate_sound: pygame.mixer.Sound,clear_sound: pygame.mixer.Sound). All method return types annotated. A_all_blocks()module-level factory function was extracted (review fix) to eliminate repeated inline[IBlock(), JBlock(), ...]list literals in__init__,get_random_block, andreset. Explicit boolean comparisons (== False,== True) removed.main.pyimport pygame,syssplit into two separate import statements. All top-level variables (screen: pygame.Surface,clock: pygame.time.Clock,game: Game,GAME_UPDATE: int, font/surface/rect variables) annotated. The entire game loop wrapped inif __name__ == "__main__":. All== True/== Falsecomparisons replaced withif game.game_over:/if not game.game_over:.Design Decisions
1.
@dataclass(frozen=True)forPositionThe
Positionclass was converted to a frozen dataclass rather than keeping it as a plain class with manual__eq__/__repr__. The dataclass decorator generates__eq__,__repr__, and__hash__with no added boilerplate, andfrozen=Trueenforces immutability consistent with howPositionis used (blocks create newPositioninstances on every move; they are never mutated in-place). The generated__hash__also allowsPositionto be used as a dict key or set member if needed in the future.2. Explicit block imports in
game.pyThe wildcard import
from blocks import *was replaced with an explicit list of all seven block classes. This makes the module's dependencies visible to mypy and ruff (which flagF403/F405on star imports), prevents namespace pollution, and satisfiesmypy --strict. The block class set is fixed (the canonical Tetris piece set is well-defined), so maintaining the explicit list is not a maintenance burden.3. Pure-logic test scope
Tests were scoped to the three modules containing no pygame display dependencies (
grid.py,block.py,game.py).conftest.pysetsSDL_VIDEODRIVER=dummyandSDL_AUDIODRIVER=dummyand patchespygame.mixer.Sound/pygame.mixer.musicwithMagicMockinstances, allowingGame.__init__to run in headless CI without audio hardware. Rendering methods (draw) and the event loop (main.py) are excluded from automated tests; they are validated by running the game manually.4.
mypy --strictfrom the startRather than an incremental annotation strategy, all seven modules were fully annotated and
strict = truewas set in[tool.mypy]in the initial commit. The codebase is only ~315 lines; the annotations are straightforward (no complex generics, no recursive types). pygame-ce 2.5.x ships withpy.typedstubs, so stub completeness was not a blocker.5.
_all_blocks()factory functionThe repeated inline block list
[IBlock(), JBlock(), LBlock(), OBlock(), SBlock(), TBlock(), ZBlock()]appeared three times ingame.py(in__init__,get_random_block, andreset). A module-level_all_blocks() -> list[Block]factory was extracted to eliminate the repetition and reduce the risk of the lists diverging.Suggested Order of Review
pyproject.toml— Establishes the toolchain baseline; review dependency pins, ruff rule selection, and mypy settings before reading any source changes.position.py— Smallest change; illustrates the dataclass conversion pattern used as the foundation for typed position objects throughout the codebase.colors.py— Simple constant annotations; no structural changes.block.py— Annotated base class; establishes attribute and method signature patterns repeated inblocks.py,grid.py, andgame.py.blocks.py— Block subclasses; review the indentation fix inLBlockand the-> Nonereturn type additions.grid.py— Annotated board state;list[list[int]]grid type and annotated mutation methods.game.py— Core game state; review the explicit import list,_all_blocks()factory, attribute annotations, and boolean idiom fixes.main.py— Entry point; review theif __name__ == "__main__":guard, split imports, boolean idiom fixes, and top-level variable annotations.tests/conftest.py— Test harness setup; SDL dummy drivers, RNG seed, mixer mocking.tests/test_grid.py,tests/test_block.py,tests/test_game.py— Test coverage for grid state, block transforms, and game logic respectively.