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.
You will build a fully working Sudoku desktop game (Tkinter GUI) with:
Entry widgetsIf you’re building a portfolio: this project demonstrates GUI layout, event handling, and clean OOP structure in a single, readable codebase.
You should be comfortable with:
__init__)If you’re still new, don’t worry—this tutorial explains why each part exists, not just what it does.
Run this project in any standard Python setup. Recommended options:
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).
No third-party libraries are required. This project uses only:
tkinter and tkinter.messageboxrandomtimeThink of your app as two screens:
Technically, this is handled inside one class:
SudokuGame: stores state (grid, solution, difficulty) and draws UIAnd 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).
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.
clear_screen)Along the way, you’ll also learn how to think in terms of UI + data + events.
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.
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)
A class-based GUI is easier to maintain because:
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:
grid is what the user sees (0 means blank)solution is the correct full solved boarddifficulty controls how many blanks we removestart_time can later become a timer featureFinally, 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.
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)
clear_screen() removes old UI (so screens don’t overlap)Frame holds the menu widgetsLabel shows textButtons start the game with different difficulty valueslambda is neededButtons 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.
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:
Pro tip: When your app grows, controller methods like this keep your project maintainable.
Sudoku generation can be complicated, but this project uses a practical approach used in many beginner and intermediate apps:
def generate_sudoku(self):
base = 3
side = base * base
Code language: PHP (php)
base = 3 because Sudoku is built from 3×3 blocksside = 9 because the board is 9×9def 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.
def shuffle(s):
return random.sample(s, len(s))
Code language: JavaScript (javascript)
This returns a shuffled copy of a list.
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:
So you get randomness without breaking Sudoku rules.
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.
solution = [row[:] for row in board]
This is one of those “developer habits” that matters:
board will be changed (we remove numbers)solution must stay untouchedsquares = 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)
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.
Now you have self.grid (the puzzle). Next step: draw it.
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.
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 cleanif 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:
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].
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:
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)
i:
set(row) removes duplicatesThis method teaches an important GUI skill: reading values from Entry widgets.
For production-grade Sudoku, validation usually also checks:
self.solutionBut for learning UI + logic flow, this checker is a simple starting point.
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:
This is a common Tkinter technique.
Instead of building multiple windows, this project uses a simple technique:
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.
if __name__ == "__main__":
root = tk.Tk()
game = SudokuGame(root)
root.mainloop()
Code language: JavaScript (javascript)
How to run:
sudoku_game.py or sudoku_game.ipynbpython sudoku_game.pyIf something doesn’t work, these are the usual reasons:
root.mainloop()command= is missing or points to the wrong methodgrid() settings need adjustment"") cause duplicates—fill all cells firstIf you want, you can also print values inside check_solution() to see what your UI is reading.
You now understand how to build a Sudoku Game using Tkinter in Python, using an OOP structure that matches real project design:
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()