# Python Sudoku Solver

I wrote this program for a Sudoku solver in Python. It utilizes tkinter GUI. I've uploaded the project to my GitHub.

Please can you have a look and let me know of any ideas or improvements you think I should make?

from tkinter import Frame,Entry,Button,messagebox

class app(Frame):
def __init__(self):     #Initialising
Frame.__init__(self)
self.grid()
self.create_grid()      #Creating the sudoku grid
self.create_buttons()       #Creating the reset and solve buttons

def create_grid(self):
self.cells = {}       #Creating a dict for the cells in the grid
self.tableheight = 9
self.tablewidth = 9
counter = 0
for row in range(self.tableheight):     #Iterating through the rows and columns creating entries
for col in range(self.tablewidth):
self.cells[counter] = Entry(self,width=5,justify='center')      #Creating the Entry
if (counter % 3==2):        #Creating extra vertical breaks to split into 3x3 boxes
else:

if ((counter // 9) == 2) or ((counter // 9) == 5):      #Creating extra horizontal breaks to split into 3x3 boxes
else:

if (counter % 9) == 0:      #Extra space to the sides of the board
elif (counter % 9) == 8:

if (counter // 9) == 0:
elif (counter // 9) == 8:

counter+=1

self.master.bind('<Up>',self.up)        #Key bindings to move between cells
self.master.bind('<Left>',self.left)
self.master.bind('<Right>',self.right)
self.master.bind('<Down>',self.down)

def up(self,event):
current_cell = str(self.focus_get())[12:]       #Splitting the string of default entry names to get the number
if len(current_cell) == 0:      #Naming started with no number at the end so setting to 1
current_cell = '1'
current_cell = int(current_cell) - 1        #Getting the current cell index
row = current_cell//9       #Finding the row the cell is on
if row == 0:        #Getting new row
new_row = 8
else:
new_row = row -1
next_cell = new_row*9 + (current_cell % 9)      #Getting index of target cell
self.cells[next_cell].focus_set()       #Setting focus to new entry

def left(self,event):
current_cell = str(self.focus_get())[12:]       #Splitting the string of default entry names to get the number
if len(current_cell) == 0:      #Naming started with no number at the end so setting to 1
current_cell = '1'
current_cell = int(current_cell) - 1        #Getting the current cell index
if (current_cell == 0):     #Index of new entry
next_cell = 80
else:
next_cell = current_cell - 1
self.cells[next_cell].focus_set()       #Setting focus to new entry

def right(self,event):
current_cell = str(self.focus_get())[12:]       #Splitting the string of default entry names to get the number
if len(current_cell) == 0:      #Naming started with no number at the end so setting to 1
current_cell = '1'
current_cell = int(current_cell) - 1        #Getting the current cell index
if (current_cell == 80):        #Index of new entry
next_cell = 0
else:
next_cell = current_cell + 1
self.cells[next_cell].focus_set()       #Setting focus to new entry

def down(self,event):
current_cell = str(self.focus_get())[12:]       #Splitting the string of default entry names to get the number
if len(current_cell) == 0:      #Naming started with no number at the end so setting to 1
current_cell = '1'
current_cell = int(current_cell) - 1        #Getting the current cell index
row = current_cell//9       #Finding the row the cell is on
if row == 8:        #Getting new row
new_row = 0
else:
new_row = row + 1
next_cell = new_row*9 + (current_cell % 9)      #Getting index of target cell
self.cells[next_cell].focus_set()       #Setting focus to new entry

def create_buttons(self):       #Creating buttons
self.reset = Button(self,text='Reset',command=self.reset_values)        #Reset
self.reset.grid(row=11,column=1)
self.solve = Button(self,text='Solve',command=self.solve_sudoku)        #Solve
self.solve.grid(row = 11, column=7)

def reset_values(self):     #Reset entries
for counter in range(81):
self.cells[counter].delete(0,'end')
self.cells[counter].configure(fg='black')

def check_values(self):     #Checks cell values are allowed
self.fetch_values ()        #Fetching values
self.cells_given = 0
for x in self.cells_list:       #Checking the number of cells intially filled is greater than 17 to allow for unique solution
if x != 0:
self.cells_given += 1
if (self.cells_given < 17):         #Less than 17 enteries results in non unique solution
messagebox.showwarning('Non Unique Solution','Entering less that 17 initial cells means the solution can not be unique')
self.different_values_given = len(list(set(self.cells_list)))       #Getting length of the list of unique cell values given
if self.different_values_given < 9:     #Checking the number of different values given is greater than 9 (8 for unique sudoku but this also counts 0)
messagebox.showwarning('Non Unique Solution','Entering less than 8 different intial values means the solution can not be unique.')
self.convert_to_board()     #Getting the list of cell values in a board format
for row in range(9):        #iterating through each cell
for col in range(9):
value = self.board[row][col]
if value!=0:
if (self.check_valid(row,col,value)):     #Check for violations of sudoku rules at start
print(row,col)
messagebox.showerror('Entry Error','Initial board contains violation of Sudoku rules')

def fetch_values(self):     #Gets cell values from board
self.cells_list = list(self.cells.values())     #Getting the cell values as a list
for x in range(81):
self.cells_list[x] = self.cells_list[x].get()
if (self.cells_list[x] not in ['','0','1','2','3','4','5','6','7','8','9']):  #Checking the cell values are allowed
messagebox.showerror('Value Error','Please ensure all enteries are between 1-9.\nLeave empty cells blank.')
self.reset_values()
raise Sudoku_Error()
if (self.cells_list[x] == ''):      #Converting empty cells to 0s
self.cells_list[x] = '0'
self.cells[x].configure(fg='blue')
self.cells_list[x] = int(self.cells_list[x])        #Converting cell values from strings to integers

def convert_to_board(self):     #Converting from a list length 81 to list of 9 lists length 9
self.board = []
for iteration in range(9):  #Iterating through different rows
row = []
for pos in range(iteration*9,(iteration+1)*9):      #Iterating through different columns in the set row
row.append(self.cells_list[pos])        #Appending to the current rows list
self.board.append(row)      #Appending row list to the board

def check_valid(self,row,col,value):        #Checking if a test value is valid

row_valid = all([value != x for x in self.board[row]])#grid[row][x] for x in range(9)])      #Check if value violates row condition
col_valid = all([value != self.board[y][col] for y in range(9)])      #Check if value violates column condition

if not(row_valid and col_valid):        #Returning false unless value is valid for both row and column constraints
return False

box_start_row,box_start_col = 3*(row//3),3*(col//3)       #Finding the position values for top left of the 3x3 box which the current test cell resides in

for y in range(box_start_row,box_start_row + 3):        #Checking if test value violates box condition
for x in range(box_start_col,box_start_col + 3):
if self.board[y][x] == value:
return False

return True

def solve_sudoku(self):        #Solving
self.check_values()     #Checking intitial
self.solve_initial()        #Initial solving algorithm

def solve_initial(self):
repeat = False      #Setting a variable to repeat if grid was changed this iteration
for row in range(9):
for col in range(9):
if self.board[row][col] == 0:
possible_values = [1,2,3,4,5,6,7,8,9]       #Setting possible values before other cell's value contraints

row_values = [self.board[row][x] for x in range(9)]       #Getting values from the cells row
col_values = [self.board[y][col] for y in range(9)]       #Getting values from the cells column
box_values = []
box_start_row,box_start_col = 3*(row//3),3*(col//3)
for y in range(box_start_row,box_start_row + 3):        #Getting values from the cells box
for x in range(box_start_col,box_start_col + 3):
box_values.append(self.board[y][x])

restricted_values = row_values + col_values + box_values        #Combining to get the total list of values not allowed
restricted_values = list(set(restricted_values))        #Removing duplicates

restricted_values.remove(0)     #Have to remove 0 as that isnt a possible value for completed suduko

for value in restricted_values:     #Removing the restricted values from the possible value list
possible_values.remove(value)

if len(possible_values) == 1:       #If only one possible value, set that value and set repeat to true
self.board[row][col] = possible_values
repeat = True

if repeat:      #Repeating if repeat is true
self.solve_initial()

if (self.solve_brute()):      #If not repeating attempt brute force to check if solved and if not then finish
return

def solve_brute(self,row = 0,col = 0):        #Solve function with inital params for row and col set

row, col = self.next_cell(row,col)

if (row == -1) and (col == -1):     #If no more cells to solve, return completed grid
return True

for value in range(1,10):
if self.check_valid(row,col,value):
self.board[row][col] = value      #Updating the cell to the new value if it is valid

if self.solve_brute(row,col):
return True
self.board[row][col] = 0      #Reset the current cell to unfilled for backtracking
return False

def next_cell(self,row,col):        #Finding the next cell to fill
for y in range(row,9):      #Check grid from min row col values for unfilled cells (treating 0 as unfilled)
for x in range(col,9):
if self.board[y][x] == 0:
return y,x
for y in range(9):      #Check entire grid for unfilled cells
for x in range(9):
if self.board[y][x] == 0:
return y,x
return -1,-1        #Returns -1,-1 if all cells filled

for y in range(9):
for x in range(9):
for pos in range(81):
if (self.cells_list[pos] == 0):

class Sudoku_Error(Exception):
pass

prog = app()
prog.master.title('Sudoku Solver')
prog.mainloop()


I strongly feel that you should separate the logic of the graphics and the algorithm of solving the sudoku. The algorithm is just some operation defined on some data structure of your choosing (actually a good way to regard solving sudokus is as extending a colouring of a graph).

I would write this algorithm to act on whatever data structure is most convenient and makes the algorithm as evident as possible.

Then have functions to adapt between the algorithm representation and the board internal representation.

Then have functions which take you from the boards internal representation to the GUI representation.

# Class Names

Classes should be in PascalCase, not lowercase. So your class should be App.

# Operator Spacing

There should be a space before and after every operator (+-*/=, etc) in your program. It improves the readability of your code greatly.

When commenting, it's common to put them a line before, so the reader sees the comment then the preceding line of code that the comment addresses. Inline comments, especially when really long like yours, are unnecessary.

# check_valid

You can greatly simplify how you check a valid sudoku board. Using any, it will return True if any of the element in the iterator are True. But in this case, since you want all False values to be checked, simply do not any(...). Take a look:

def check_valid(self,row,col,value):

row_valid = all([value != x for x in self.board[row]])
col_valid = all([value != self.board[y][col] for y in range(9)])

if not(row_valid and col_valid):
return False

box_start_row, box_start_col = 3 * (row // 3), 3 * (col // 3)

return not any(
self.board[y][x] == value
for y in range(box_start_row, box_start_row + 3)
for x in range(box_start_col, box_start_col + 3)
)


# Contants

I see you've defined self.tableheight and self.tablewidth as constants (although they should be snake_case then UPPER_CASE). That's good. But you only use them once and never again! Then you have stray 9 values all over your code. You should utilize these class constants.
{"ft_lang_label":"__label__en","ft_lang_prob":0.5470192,"math_prob":0.88284135,"size":9847,"snap":"2021-43-2021-49","text_gpt3_token_len":2631,"char_repetition_ratio":0.1723052,"word_repetition_ratio":0.19700375,"special_character_ratio":0.2862801,"punctuation_ratio":0.15643045,"nsfw_num_words":0,"has_unicode_error":false,"math_prob_llama3":0.97850704,"pos_list":[0],"im_url_duplicate_count":[null],"WARC_HEADER":"{\"WARC-Type\":\"response\",\"WARC-Date\":\"2021-10-24T14:53:48Z\",\"WARC-Record-ID\":\"<urn:uuid:6db9c78d-0d32-4aa1-b669-cd9b5c25a912>\",\"Content-Length\":\"190679\",\"Content-Type\":\"application/http; msgtype=response\",\"WARC-Warcinfo-ID\":\"<urn:uuid:196c7a5f-8659-466a-8a79-bacbeddad0a5>\",\"WARC-Concurrent-To\":\"<urn:uuid:bfe2fbd3-0eab-4fe7-8458-235a82b94f79>\",\"WARC-IP-Address\":\"151.101.193.69\",\"WARC-Target-URI\":\"https://codereview.stackexchange.com/questions/235133/python-sudoku-solver/237518#237518\",\"WARC-Payload-Digest\":\"sha1:HOEJM2NMW7NZKR3HBTQDODOA3LM7UYUN\",\"WARC-Block-Digest\":\"sha1:WZMEOYAEVRQI4MAUN2PIZ6V3AAVJOB4V\",\"WARC-Identified-Payload-Type\":\"text/html\",\"warc_filename\":\"/cc_download/warc_2021/CC-MAIN-2021-43/CC-MAIN-2021-43_segments_1634323586043.75_warc_CC-MAIN-20211024142824-20211024172824-00178.warc.gz\"}"}