If you want to learn Python GUI development and build a real project that feels like an app, a Sudoku game using Tkinter is a great choice. It combines user interface design, event handling, and a bit of logic to generate and validate puzzles.
In this tutorial, you will learn how to build a Sudoku Game using Tkinter (Python’s standard GUI library). We’ll walk through the complete code structure: from the difficulty selection screen to generating a Sudoku grid, rendering it as input cells, and checking the user’s solution.
This explanation is written for students, professional developers, and educators—so it keeps the language simple, but still explains the design clearly and correctly.
What You Will Build
You will build a fully working Sudoku desktop game (Tkinter GUI) with:
- A start screen to choose difficulty: Easy / Medium / Hard
- A 9×9 Sudoku board created using **Tkinter **
Entrywidgets - Pre-filled numbers that are locked (users cannot change them)
- Buttons for:
- Check Solution (validate the filled grid)
- Quit & Show Solution (reveal the solved grid)
- New Game (go back to difficulty selection)
- A Sudoku generator that:
- creates a valid solved Sudoku
- removes numbers based on the chosen difficulty
If you’re building a portfolio: this project demonstrates GUI layout, event handling, and clean OOP structure in a single, readable codebase.
Prerequisites
Knowledge prerequisites
You should be comfortable with:
- Python basics (variables, loops, functions)
- Lists and nested lists (2D lists for a 9×9 grid)
- Basic OOP (classes, methods,
__init__)
If you’re still new, don’t worry—this tutorial explains why each part exists, not just what it does.
Tools / editor prerequisites
Run this project in any standard Python setup. Recommended options:
- VS Code (popular for students + professionals; great Python extensions)
- PyCharm Community Edition (strong IDE features)
- Anaconda (use Spyder or run scripts via Anaconda Prompt)
- IDLE (ships with Python; basic but works)
Tip: Create a file named sudoku_game.py, paste code, and run it as a normal Python script. Tkinter apps behave best in script mode (not inside notebooks).
Libraries used
No third-party libraries are required. This project uses only:
tkinterandtkinter.messageboxrandomtime
Full Project Structure
Think of your app as two screens:
- Start Menu Screen → user chooses difficulty
- Game Screen → Sudoku grid + buttons
Technically, this is handled inside one class:
SudokuGame: stores state (grid, solution, difficulty) and draws UI
And one entry point:
root = tk.Tk()game = SudokuGame(root)root.mainloop()
Tkinter is event-driven. Your code sets up widgets and callbacks, and then Tkinter waits for user actions (clicks and typing).
Step-by-Step: Build the SudokuGame Class
We’ll build it like a real software project—one layer at a time. If you follow this sequence, the code will feel natural instead of overwhelming.
- Imports
- Create the class + window configuration
- Start menu (difficulty selection)
- Start game controller method
- Sudoku generator (solution + puzzle)
- Render the 9×9 board in Tkinter
- Add buttons and connect events
- Implement checking and solution reveal
- Screen switching helper (
clear_screen) - App entry point
Along the way, you’ll also learn how to think in terms of UI + data + events.
Imports: Tkinter, Messagebox, Random, Time
Start with:
import tkinter as tk
from tkinter import messagebox
import random
import time
Code language: JavaScript (javascript)
What these give you:
tk: widgets (Frame,Label,Button,Entry)messagebox: clean pop-ups (like a real app)random: shuffle and remove cells for puzzle difficultytime: start-time storage (useful if you later add a timer)
Quick checkpoint: If you can run a simple Tkinter window, you’re ready. If not, install Python properly and re-check your environment.
Create the Class + Configure the Window
Here’s the foundation:
class SudokuGame:
def __init__(self, master):
self.master = master
self.master.title("Sudoku Game")
self.master.geometry("500x550")
self.master.resizable(False, False)
Why this is a professional structure
A class-based GUI is easier to maintain because:
- You keep UI and state together (clean design)
- You avoid global variables
- It’s easier to add features later (timer, hints, reset, highlighting)
Initialize state (your “game memory”)
Now add:
self.difficulty = None
self.start_time = None
self.grid = [[0]*9 for _ in range(9)]
self.solution = None
Code language: PHP (php)
How to read this as a developer:
gridis what the user sees (0 means blank)solutionis the correct full solved boarddifficultycontrols how many blanks we removestart_timecan later become a timer feature
Finally, show the first screen:
self.create_start_menu()
Code language: CSS (css)
This is an important idea in GUI apps: the constructor sets up initial state and draws the first view.
Build the Start Menu Screen (Difficulty Selection)
A Sudoku app feels much more complete with a difficulty screen. It also keeps your UI clean: menu UI and game UI are separate.
def create_start_menu(self):
self.clear_screen()
self.start_menu = tk.Frame(self.master)
self.start_menu.pack()
tk.Label(self.start_menu, text="Select Difficulty:", font=("Arial", 14)).pack()
tk.Button(self.start_menu, text="Easy",
command=lambda: self.start_game("easy"), width=15).pack()
tk.Button(self.start_menu, text="Medium",
command=lambda: self.start_game("medium"), width=15).pack()
tk.Button(self.start_menu, text="Hard",
command=lambda: self.start_game("hard"), width=15).pack()
Code language: PHP (php)
What’s happening here (in plain terms)
clear_screen()removes old UI (so screens don’t overlap)- A
Frameholds the menu widgets - One
Labelshows text - Three
Buttonsstart the game with different difficulty values
Why lambda is needed
Buttons call functions later, when clicked. If you want to pass parameters, you wrap the call:
command=lambda: self.start_game("easy")
Code language: PHP (php)
Otherwise, Tkinter would try to run the function immediately.
Mini-challenge: Change button widths or font size and see how the UI changes.
Start the Game (The Controller Method)
This method connects menu → puzzle generation → board UI.
def start_game(self, difficulty):
self.difficulty = difficulty
self.start_time = time.time()
self.clear_screen()
self.grid, self.solution = self.generate_sudoku()
self.create_game_board()
Code language: PHP (php)
Why this method is well-designed:
- It keeps the flow readable
- Each major step is delegated to a separate method
- If you ever add features (timer label, score, hints), this becomes the central place to initialize them
Pro tip: When your app grows, controller methods like this keep your project maintainable.
Generate a Sudoku Puzzle (Solution First, Then Remove Cells)
Sudoku generation can be complicated, but this project uses a practical approach used in many beginner and intermediate apps:
- Build a valid full solution board
- Remove numbers based on difficulty
Setup
def generate_sudoku(self):
base = 3
side = base * base
Code language: PHP (php)
base = 3because Sudoku is built from 3×3 blocksside = 9because the board is 9×9
Pattern function (the engine)
def pattern(r, c):
return (base * (r % base) + r // base + c) % side
Code language: JavaScript (javascript)
Think of pattern() as a blueprint that knows how to place numbers in a Sudoku-like structure.
Shuffle helper
def shuffle(s):
return random.sample(s, len(s))
Code language: JavaScript (javascript)
This returns a shuffled copy of a list.
Shuffle rows and columns (but in Sudoku-safe ways)
r_base = range(base)
rows = [g * base + r for g in shuffle(r_base) for r in shuffle(r_base)]
cols = [g * base + c for g in shuffle(r_base) for c in shuffle(r_base)]
Why this is safe:
- Sudoku allows swapping row-groups and rows inside a group
- Same for columns
So you get randomness without breaking Sudoku rules.
Fill the board
nums = shuffle(range(1, side + 1))
board = [[nums[pattern(r, c)] for c in cols] for r in rows]
At this point, board is a complete solved Sudoku.
Save the solution (deep copy)
solution = [row[:] for row in board]
This is one of those “developer habits” that matters:
boardwill be changed (we remove numbers)solutionmust stay untouched
Remove numbers based on difficulty
squares = side * side
empties = squares * (0.3 if self.difficulty == "easy" else
0.5 if self.difficulty == "medium" else 0.7)
for p in random.sample(range(squares), int(empties)):
board[p // side][p % side] = 0
return board, solution
Code language: PHP (php)
- Easy → fewer blanks (more clues)
- Hard → more blanks (less help)
Developer note: This guarantees a valid solution exists (because the puzzle comes from a valid solved board). However, uniqueness is not guaranteed without additional solver checks.
Render the 9×9 Sudoku Grid in Tkinter
Now you have self.grid (the puzzle). Next step: draw it.
Create a frame container
def create_game_board(self):
self.board_frame = tk.Frame(self.master)
self.board_frame.pack(pady=10)
Code language: PHP (php)
A frame is like a “board area” where the grid lives.
Create 81 Entry widgets
self.cells = []
cell_size = 50
for i in range(9):
row = []
for j in range(9):
entry = tk.Entry(self.board_frame, width=2,
font=("Arial", 18), justify='center', bd=2)
entry.grid(row=i, column=j,
padx=(2 if j % 3 == 0 else 0),
pady=(2 if i % 3 == 0 else 0),
ipadx=cell_size//10, ipady=cell_size//10)
Code language: PHP (php)
What makes it feel like Sudoku:
grid()layout matches Sudoku naturallyjustify='center'makes numbers look clean- spacing every 3 rows/cols visually separates 3×3 blocks
Lock the clue cells
if self.grid[i][j] != 0:
entry.insert(0, str(self.grid[i][j]))
entry.config(state='disabled',
disabledbackground='lightgray',
disabledforeground='black')
else:
entry.config(bg='white')
Code language: PHP (php)
This is a key UX detail:
- Locked cells feel like “given clues”
- Editable cells feel like “your move”
Store references to cells
row.append(entry)
...
self.cells.append(row)
Code language: CSS (css)
This 2D list becomes your “UI-to-data bridge.” Any time you want to read or update cells, you use self.cells[i][j].
Add Buttons (Your User Controls)
Buttons make the game interactive. Without them, the board is just a grid.
self.check_button = tk.Button(self.master, text="Check Solution",
command=self.check_solution)
self.check_button.pack(pady=5)
self.quit_button = tk.Button(self.master, text="Quit & Show Solution",
command=self.show_solution)
self.quit_button.pack(pady=5)
self.new_game_button = tk.Button(self.master, text="New Game",
command=self.create_start_menu)
self.new_game_button.pack(pady=5)
Code language: PHP (php)
Engaging way to think about it:
- Check Solution: “Did I solve it correctly?”
- Quit & Show Solution: “I’m stuck. Teach me the answer.”
- New Game: “Let’s restart with a fresh puzzle.”
Validate the Board (Check Solution)
Here’s the project’s checker:
def check_solution(self):
for i in range(9):
row = [self.cells[i][j].get() for j in range(9)]
col = [self.cells[j][i].get() for j in range(9)]
if len(set(row)) != 9 or len(set(col)) != 9:
messagebox.showerror("Error", "Invalid Solution")
return
messagebox.showinfo("Success", "Correct Solution!")
Code language: PHP (php)
How to read this like a developer
- For each index
i:- build the current row values
- build the current column values
set(row)removes duplicates- if duplicates exist (or blanks repeat), it fails
This method teaches an important GUI skill: reading values from Entry widgets.
Real-world note
For production-grade Sudoku, validation usually also checks:
- inputs are digits 1–9
- no empty strings
- each 3×3 subgrid
- and/or compare with
self.solution
But for learning UI + logic flow, this checker is a simple starting point.
Reveal the Correct Solution (Show Solution)
When users want to see the answer, this fills the board from self.solution:
def show_solution(self):
for i in range(9):
for j in range(9):
self.cells[i][j].config(state='normal')
self.cells[i][j].delete(0, tk.END)
self.cells[i][j].insert(0, str(self.solution[i][j]))
self.cells[i][j].config(state='disabled',
disabledbackground='lightgray',
disabledforeground='black')
messagebox.showinfo("Solution", "Here is the correct solution!")
Code language: PHP (php)
Notice the professional detail:
- even disabled cells are temporarily enabled to update them
- then disabled again
This is a common Tkinter technique.
Screen Switching Helper (Clear Screen)
Instead of building multiple windows, this project uses a simple technique:
- destroy all widgets
- rebuild the next screen
def clear_screen(self):
for widget in self.master.winfo_children():
widget.destroy()
Code language: CSS (css)
This keeps your app easy to understand and avoids complex navigation logic.
Run the App (Entry Point)
if __name__ == "__main__":
root = tk.Tk()
game = SudokuGame(root)
root.mainloop()
Code language: JavaScript (javascript)
How to run:
- Save the file as
sudoku_game.pyorsudoku_game.ipynb - Run:
- VS Code: click Run / Python
- Terminal:
python sudoku_game.py
Common Mistakes and Quick Debug Tips
If something doesn’t work, these are the usual reasons:
- Window opens and immediately closes: you forgot
root.mainloop() - Buttons do nothing:
command=is missing or points to the wrong method - Grid looks messy: spacing and
grid()settings need adjustment - Validation always fails: blank cells (
"") cause duplicates—fill all cells first
If you want, you can also print values inside check_solution() to see what your UI is reading.
Summary
You now understand how to build a Sudoku Game using Tkinter in Python, using an OOP structure that matches real project design:
- start menu → choose difficulty
- generate puzzle + save solution
- draw 9×9 Entry grid
- lock clues, allow user input
- check solution and reveal solution
Complete SudokuGame Code
Below is the complete working code in one place.
import tkinter as tk
from tkinter import messagebox
import random
import time
class SudokuGame:
def __init__(self, master):
self.master = master
self.master.title("Sudoku Game")
self.master.geometry("500x550")
self.master.resizable(False, False)
self.difficulty = None
self.start_time = None
self.grid = [[0]*9 for _ in range(9)]
self.solution = None
self.create_start_menu()
def create_start_menu(self):
self.clear_screen()
self.start_menu = tk.Frame(self.master)
self.start_menu.pack()
tk.Label(self.start_menu, text="Select Difficulty:", font=("Arial", 14)).pack()
tk.Button(self.start_menu, text="Easy", command=lambda: self.start_game("easy"), width=15).pack()
tk.Button(self.start_menu, text="Medium", command=lambda: self.start_game("medium"), width=15).pack()
tk.Button(self.start_menu, text="Hard", command=lambda: self.start_game("hard"), width=15).pack()
def start_game(self, difficulty):
self.difficulty = difficulty
self.start_time = time.time()
self.clear_screen()
self.grid, self.solution = self.generate_sudoku()
self.create_game_board()
def generate_sudoku(self):
base = 3
side = base * base
def pattern(r, c): return (base * (r % base) + r // base + c) % side
def shuffle(s): return random.sample(s, len(s))
r_base = range(base)
rows = [g * base + r for g in shuffle(r_base) for r in shuffle(r_base)]
cols = [g * base + c for g in shuffle(r_base) for c in shuffle(r_base)]
nums = shuffle(range(1, side + 1))
board = [[nums[pattern(r, c)] for c in cols] for r in rows]
solution = [row[:] for row in board]
squares = side * side
empties = squares * (0.3 if self.difficulty == "easy" else 0.5 if self.difficulty == "medium" else 0.7)
for p in random.sample(range(squares), int(empties)):
board[p // side][p % side] = 0
return board, solution
def create_game_board(self):
self.board_frame = tk.Frame(self.master)
self.board_frame.pack(pady=10)
self.cells = []
cell_size = 50
for i in range(9):
row = []
for j in range(9):
entry = tk.Entry(self.board_frame, width=2, font=("Arial", 18), justify='center', bd=2)
entry.grid(row=i, column=j, padx=(2 if j % 3 == 0 else 0), pady=(2 if i % 3 == 0 else 0), ipadx=cell_size//10, ipady=cell_size//10)
if self.grid[i][j] != 0:
entry.insert(0, str(self.grid[i][j]))
entry.config(state='disabled', disabledbackground='lightgray', disabledforeground='black')
else:
entry.config(bg='white')
row.append(entry)
self.cells.append(row)
self.check_button = tk.Button(self.master, text="Check Solution", command=self.check_solution)
self.check_button.pack(pady=5)
self.quit_button = tk.Button(self.master, text="Quit & Show Solution", command=self.show_solution)
self.quit_button.pack(pady=5)
self.new_game_button = tk.Button(self.master, text="New Game", command=self.create_start_menu)
self.new_game_button.pack(pady=5)
def check_solution(self):
for i in range(9):
row = [self.cells[i][j].get() for j in range(9)]
col = [self.cells[j][i].get() for j in range(9)]
if len(set(row)) != 9 or len(set(col)) != 9:
messagebox.showerror("Error", "Invalid Solution")
return
messagebox.showinfo("Success", "Correct Solution!")
def show_solution(self):
for i in range(9):
for j in range(9):
self.cells[i][j].config(state='normal')
self.cells[i][j].delete(0, tk.END)
self.cells[i][j].insert(0, str(self.solution[i][j]))
self.cells[i][j].config(state='disabled', disabledbackground='lightgray', disabledforeground='black')
messagebox.showinfo("Solution", "Here is the correct solution!")
def clear_screen(self):
for widget in self.master.winfo_children():
widget.destroy()
if __name__ == "__main__":
root = tk.Tk()
game = SudokuGame(root)
root.mainloop()
