Scrollable Nav Bar

Sudoku Game In Python Using Tkinter With Source Code

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 **Entry widgets
  • 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:

  • tkinter and tkinter.messagebox
  • random
  • time

Full Project Structure

Think of your app as two screens:

  1. Start Menu Screen → user chooses difficulty
  2. 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.

  1. Imports
  2. Create the class + window configuration
  3. Start menu (difficulty selection)
  4. Start game controller method
  5. Sudoku generator (solution + puzzle)
  6. Render the 9×9 board in Tkinter
  7. Add buttons and connect events
  8. Implement checking and solution reveal
  9. Screen switching helper (clear_screen)
  10. 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 difficulty
  • time: 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:

  • grid is what the user sees (0 means blank)
  • solution is the correct full solved board
  • difficulty controls how many blanks we remove
  • start_time can 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 Frame holds the menu widgets
  • One Label shows text
  • Three Buttons start 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:

  1. Build a valid full solution board
  2. Remove numbers based on difficulty

Setup

def generate_sudoku(self):
    base = 3
    side = base * base
Code language: PHP (php)
  • base = 3 because Sudoku is built from 3×3 blocks
  • side = 9 because 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:

  • board will be changed (we remove numbers)
  • solution must 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 naturally
  • justify='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:

  1. Save the file as sudoku_game.py or sudoku_game.ipynb
  2. 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()