Build a Sudoku Puzzle Generator in Python

Ever stared at a Sudoku puzzle and thought, "I could totally make one of these with Python..."? Well, you're about to. If you’ve got Python and a bit of curiosity, you absolutely can. This article walks you through building a complete Sudoku puzzle generator and solver.

No third-party packages, no setup, no fluff. Just pure Python and a whole lot of logic fun.

What Are We Creating?

By the time we’re done, you'll have a Python script that not only creates fully valid Sudoku boards from scratch but also removes just the right amount of numbers to make a playable puzzle and then solves it too.

It even supports difficulty levels like easy, medium, and hard, and prints the whole thing out in a clean, readable format. It’s one of those rare projects that's as satisfying to see run as it is to build.

The Game Plan

Here’s how we’ll break it down:

  1. Start with generating a valid, fully filled Sudoku board.
  2. Remove cells to create the actual puzzle, with difficulty baked in.
  3. Add a backtracking algorithm to solve the puzzle.
  4. Print everything nicely to the console so it’s easy on the eyes.

No black-box libraries. Just logic and clean code.

Quick Sudoku Refresher

If you’ve ever played Sudoku, you already know the basics. But let’s recap for clarity: a Sudoku board is a 9×9 grid split into nine 3×3 boxes. The rules are simple—but strict.

Each row, column, and 3×3 box must contain the digits 1 through 9, with no repeats. And a proper puzzle must have exactly one unique solution.

Let’s Talk Code

Here’s the entire Python script you’ll need. It's broken down below so we can walk through how it all works, but feel free to run it as-is to see the magic happen:

import random
import copy

# Constants for board dimensions
BOARD_SIZE = 9
BOX_SIZE = 3  # Size of the smaller 3x3 boxes

# Check if placing 'num' at board[row][col] is valid according to Sudoku rules
def is_valid(board, row, col, num):
    # Check the row and column
    for i in range(BOARD_SIZE):
        if board[row][i] == num or board[i][col] == num:
            return False

    # Check the 3x3 box the cell belongs to
    box_row_start = (row // BOX_SIZE) * BOX_SIZE
    box_col_start = (col // BOX_SIZE) * BOX_SIZE

    for i in range(BOX_SIZE):
        for j in range(BOX_SIZE):
            if board[box_row_start + i][box_col_start + j] == num:
                return False

    return True  # No conflicts found

# Solve the Sudoku using backtracking
def solve(board):
    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE):
            if board[row][col] == 0:  # Empty cell
                for num in range(1, 10):  # Try numbers 1-9
                    if is_valid(board, row, col, num):
                        board[row][col] = num
                        if solve(board):
                            return True  # Puzzle solved!
                        board[row][col] = 0  # Backtrack if not solvable
                return False  # No valid number found, backtrack
    return True  # No empty cells left; puzzle is solved

# Create a full, valid Sudoku board
def generate_full_board():
    board = [[0] * BOARD_SIZE for _ in range(BOARD_SIZE)]  # Start with all zeros
    fill_board(board)
    return board

# Recursive function to fill the board with valid numbers
def fill_board(board):
    numbers = list(range(1, 10))  # Numbers to place

    for row in range(BOARD_SIZE):
        for col in range(BOARD_SIZE):
            if board[row][col] == 0:
                random.shuffle(numbers)  # Randomize number order for variety
                for num in numbers:
                    if is_valid(board, row, col, num):
                        board[row][col] = num
                        if fill_board(board):
                            return True  # Keep going recursively
                        board[row][col] = 0  # Backtrack
                return False  # Trigger backtracking if stuck
    return True  # Board successfully filled

# Remove cells from the board to create the puzzle
def remove_cells(board, num_holes):
    holes = 0
    while holes < num_holes:
        row = random.randint(0, 8)
        col = random.randint(0, 8)
        if board[row][col] != 0:
            board[row][col] = 0  # Remove cell
            holes += 1
    return board

# Print the board in a clean, readable format
def print_board(board):
    for i in range(BOARD_SIZE):
        row = ''
        for j in range(BOARD_SIZE):
            val = board[i][j]
            row += '.' if val == 0 else str(val)  # Use '.' for empty cells
            row += ' '
            if j % 3 == 2 and j != 8:  # Add vertical dividers
                row += '| '
        print(row)
        if i % 3 == 2 and i != 8:  # Add horizontal dividers
            print('-' * 21)

# Generate a puzzle based on difficulty level
def generate_puzzle(difficulty='medium'):
    full_board = generate_full_board()  # Generate completed board

    # Map difficulty to number of cells to remove
    difficulty_map = {
        'easy': 30,
        'medium': 40,
        'hard': 55
    }

    holes = difficulty_map.get(difficulty.lower(), 40)  # Default to medium
    puzzle = remove_cells(copy.deepcopy(full_board), num_holes=holes)
    return puzzle, full_board  # Return the puzzle and the complete solution

# ===== Main Logic =====

difficulty = 'hard'  # Choose difficulty: 'easy', 'medium', or 'hard'
print(f"Difficulty: {difficulty.capitalize()}\n")

# Generate puzzle and get its full solution
puzzle, solution_template = generate_puzzle(difficulty)

print("Generated Sudoku Puzzle:\n")
print_board(puzzle)  # Show puzzle with missing numbers

# Try to solve the puzzle
solution = copy.deepcopy(puzzle)
if solve(solution):
    print("\nSolved Sudoku Board:\n")
    print_board(solution)
else:
    print("\nNo solution found.")

How It All Works

Let’s peek under the hood of each function so you’re not just copying code, but actually understanding the engine.

is_valid(board, row, col, num)

This function is your rule enforcer. It checks whether a given number can be legally placed at a certain cell, without breaking Sudoku’s “no duplicates” rule across rows, columns, or 3×3 boxes.

solve(board)

This is the brain of your Sudoku solver. It uses a classic backtracking algorithm: scan the board for an empty cell, try numbers 1–9, and place one if it fits. If none fit, it backtracks to the last placed number and tries something else. Rinse and repeat until the board is complete or proves unsolvable.

generate_full_board() and fill_board(board)

These two functions tag-team to generate a complete, valid Sudoku board. You start with an empty 9x9 grid and use recursive backtracking to fill it in. The twist? The number list is shuffled every time, so you’ll get a different board with each run.

remove_cells(board, num_holes)

Once the full board is built, this function randomly removes a set number of cells, creating the actual playable puzzle. The more numbers you remove, the harder it gets.

For example, an “easy” board might keep 50+ clues. A “hard” one might leave you with just 26.

print_board(board)

This function is pure Polish. It turns your raw data into a readable Sudoku grid using dots for blank spaces and lines to clearly separate each 3×3 box. Trust me—it makes all the difference.

generate_puzzle(difficulty='medium')

This is your all-in-one setup function. Pick a difficulty level, and it generates both a puzzle and its complete solution, ready to print, play, or solve.

Tweak the Difficulty

Want an easier or tougher challenge? Just change this line:

difficulty = 'hard'   # Try 'easy' or 'medium'
  • Easy: Around 30 missing cells.
  • Medium: About 40.
  • Hard: Up to 55 blanks prepare to squint and think!

Ideas to Take It Further

Once you've got this working, you might want to take it beyond the terminal. Here are a few fun next steps:

  • Export puzzles to .txt or .csv files for sharing.
  • Add logic to ensure that generated puzzles have exactly one solution.
  • Turn it into a full-fledged puzzle game with a difficulty selector and timer.

If any of those sound exciting, drop a comment, and I might just do a follow-up article!

Wrapping Up

This isn’t just a cool Python project. It’s a full exercise in thinking like a problem solverby building something from scratch that feels... almost human. Best part? You can run it anywhere, even inside a browser. No installs. No dependencies. Just Python.

Happy puzzling!