Sudoku Solver in Python
Intermediate · Logic and puzzles · about 60 minutes to build · 149 lines
Type a Sudoku puzzle row by row, or use the built-in one, and the program fills in every empty cell.
The program
Press Run and answer its questions in the box that appears under the code.
Open it in the full editor to save it, share it and keep building.
Sample run
What the program printed, and what was typed (in yellow).
Sudoku solver 1) Solve the example 2) Type a puzzle 3) Quit Choose 1-3: 1 Puzzle Solution 5 3 . | . 7 . | . . . 5 3 4 | 6 7 8 | 9 1 2 6 . . | 1 9 5 | . . . 6 7 2 | 1 9 5 | 3 4 8 . 9 8 | . . . | . 6 . 1 9 8 | 3 4 2 | 5 6 7 ------+-------+------ ------+-------+------ 8 . . | . 6 . | . . 3 8 5 9 | 7 6 1 | 4 2 3 4 . . | 8 . 3 | . . 1 4 2 6 | 8 5 3 | 7 9 1 7 . . | . 2 . | . . 6 7 1 3 | 9 2 4 | 8 5 6 ------+-------+------ ------+-------+------ . 6 . | . . . | 2 8 . 9 6 1 | 5 3 7 | 2 8 4 . . . | 4 1 9 | . . 5 2 8 7 | 4 1 9 | 6 3 5 . . . | . 8 . | . 7 9 3 4 5 | 2 8 6 | 1 7 9 Solved in 4,208 tries. 1) Solve the example 2) Type a puzzle 3) Quit Choose 1-3: 2 Type 9 characters per row: digits, and . or 0 for an empty cell. Row 1: ..3.2.6.. Row 2: 9..3.5..1 Row 3: ..18.64 A row needs exactly 9 digits or dots, like 53..7.... Row 3: ..18.64.. Row 4: ..81.29.. Row 5: 7.......8 Row 6: ..67.82.. Row 7: ..26.95.. Row 8: 8..2.3..9 Row 9: ..5.1.3.. Puzzle Solution . . 3 | . 2 . | 6 . . 4 8 3 | 9 2 1 | 6 5 7 9 . . | 3 . 5 | . . 1 9 6 7 | 3 4 5 | 8 2 1 . . 1 | 8 . 6 | 4 . . 2 5 1 | 8 7 6 | 4 9 3 ------+-------+------ ------+-------+------ . . 8 | 1 . 2 | 9 . . 5 4 8 | 1 3 2 | 9 7 6 7 . . | . . . | . . 8 7 2 9 | 5 6 4 | 1 3 8 . . 6 | 7 . 8 | 2 . . 1 3 6 | 7 9 8 | 2 4 5 ------+-------+------ ------+-------+------ . . 2 | 6 . 9 | 5 . . 3 7 2 | 6 8 9 | 5 1 4 8 . . | 2 . 3 | . . 9 8 1 4 | 2 5 3 | 7 6 9 . . 5 | . 1 . | 3 . . 6 9 5 | 4 1 7 | 3 8 2 Solved in 200 tries. 1) Solve the example 2) Type a puzzle 3) Quit Choose 1-3: 3 Goodbye.
How it is built
1. Reading a row
A row is 9 characters: digits for the clues and ., 0 or _ for empty cells, which become 0 on the board. Spaces and | are removed first, so rows can be typed with gaps.
def parse_row(text): """Turn "53..7...." into [5, 3, 0, 0, 7, 0, 0, 0, 0], or None.""" text = text.replace(" ", "").replace("|", "") if len(text) != SIZE or not all(char in ALLOWED for char in text): return None return [0 if char in BLANKS else int(char) for char in text]
2. The three rules
A digit may go in a cell only if it is not already in the same row, the same column or the same 3x3 box. row - row % BOX gives the top row of the box (0, 3 or 6), and the same trick gives its left column.
def can_place(board, row, col, digit): """Check that digit is not already in this row, column or 3x3 box.""" if digit in board[row]: return False if any(board[r][col] == digit for r in range(SIZE)): return False top, left = row - row % BOX, col - col % BOX return all(board[r][c] != digit for r in range(top, top + BOX) for c in range(left, left + BOX))
3. Backtracking
The solver finds the first empty cell and tries the digits 1 to 9. When a digit fits, it is written in and solve calls itself to fill the rest of the board. If that fails, the digit is erased and the next one tried; if no digit fits, it returns False, and the call before it moves on to its own next digit.
def solve(board, stats): """Fill the board by trying digits and undoing the ones that fail.""" cell = find_empty(board) if cell is None: return True # no empty cells left: solved row, col = cell for digit in range(1, SIZE + 1): if stats["tries"] >= MAX_TRIES: return False # taking too long: give up if can_place(board, row, col, digit): board[row][col] = digit stats["tries"] += 1 if solve(board, stats): return True board[row][col] = 0 # dead end: undo it and try the next digit return False # no digit fits here, so an earlier guess was wrong
4. Checking the clues
A puzzle typed with two 5s in one row can never be solved, and the search could take a long time to find that out. So each clue is lifted out and checked against the others before solving starts.
def clues_are_valid(board): """Check that the given digits do not already break the rules.""" for row in range(SIZE): for col in range(SIZE): digit = board[row][col] if digit: board[row][col] = 0 # lift it out to check it against the rest ok = can_place(board, row, col, digit) board[row][col] = digit if not ok: return False return True
5. Drawing the board
Each row becomes a line of text with | between the boxes, and a dashed line goes in after every third row. Returning lines instead of printing them lets the puzzle and its solution sit side by side.
def board_lines(board): """Draw the board as text lines with a line between the 3x3 boxes.""" lines = [] for row in range(SIZE): if row and row % BOX == 0: lines.append("------+-------+------") cells = [str(n) if n else "." for n in board[row]] boxes = [" ".join(cells[i:i + BOX]) for i in range(0, SIZE, BOX)] lines.append(" | ".join(boxes)) return lines
6. Solving and showing
stats is a dictionary passed into solve, so the count of tries can grow inside every level of the recursion. MAX_TRIES stops a puzzle that would take too long.
def solve_and_show(board): """Solve a puzzle and print it next to its solution.""" if not clues_are_valid(board): print("Two clues clash: a digit repeats in a row, column or box.") return puzzle = board_lines(board) stats = {"tries": 0} if not solve(board, stats): if stats["tries"] >= MAX_TRIES: print(f"No solution found in {MAX_TRIES:,} tries, so I stopped.") else: print("This puzzle has no solution.") return print(f"\n{'Puzzle':<21} Solution") for left, right in zip(puzzle, board_lines(board)): print(f"{left:<21} {right}") print(f"Solved in {stats['tries']:,} tries.") # a try = one digit placed
Make it bigger
- Fill the empty cell with the fewest possible digits first, as people do, and compare how many tries it saves.
- Count all the solutions instead of stopping at the first, to check that a puzzle has only one.
- Make new puzzles: solve an empty board with the digits in random order, then remove clues.
- Accept a whole puzzle pasted as one line of 81 characters.
What it practises
- recursion
- backtracking
- lists of lists
- any() and all()
- list comprehensions