from traitlets import Unicode, Bool, Int, List, validate, observe, TraitError, All
from ipywidgets import DOMWidget, register
import copy
@register
class Sudoku(DOMWidget):
= Unicode('SudokuView').tag(sync=True)
_view_name = Unicode('sudoku_widget').tag(sync=True)
_view_module = Unicode('0.1.0').tag(sync=True)
_view_module_version
# Attributes
= List(trait=Bool(), default_value=[False] * 81, minlen=81, maxlen=81, help="A list of booleans that indicate whether a value is part of the puzzle.").tag(sync=True)
fixed = List(trait=Int(), default_value=[0] * 81, minlen=81, maxlen=81, help="A list of integers for each cell.").tag(sync=True)
_value = Bool(False, help="Enable or disable user changes.").tag(sync=True)
disabled
# Basic validator for value
@validate('_value')
def _valid_value(self, proposal):
for i in proposal['value']:
if i < 0 or i > 9:
raise TraitError('Invalid value: all elements must be numbers from 0 to 9')
return proposal['value']
@property
def value(self):
return copy.deepcopy(self._value)
@value.setter
def value(self, v):
self._value = v
def __init__(self,*args,**kwargs):
'_value'] = kwargs.pop('value', [0]*81)
kwargs[__init__(self,*args,**kwargs)
DOMWidget.
def __getitem__(self,index):
return self._value[index]
def __setitem__(self,index,val):
= self.value
vals = val
vals[index] self._value = vals
In this post I’ll demonstrate how to build a custom Jupyter widget for displaying and editing Sudoku puzzles. I’ll also show how to create a Sudoku solver that uses this widget.
How to play Sudoku
Only read this if you’ve been living under a rock, otherwise skip to the good stuff.
In Sudoku, the objective is to fill a 9x9 grid with digits so that each column, each row, and each of the nine 3x3 blocks that compose the grid contain all of the digits from 1 to 9.
An example Sudoku puzzle:
8 | 5 | 6 | 1 | |||||
9 | 4 | |||||||
2 | 3 | 8 | ||||||
4 | 2 | |||||||
7 | 9 | 5 | ||||||
3 | 8 | |||||||
5 | 8 | |||||||
7 | 1 | |||||||
6 | 4 |
The solution to this puzzle looks like this:
3 | 8 | 5 | 9 | 6 | 1 | 4 | 2 | 7 |
9 | 2 | 4 | 8 | 7 | 3 | 1 | 5 | 6 |
1 | 6 | 7 | 5 | 4 | 2 | 3 | 9 | 8 |
5 | 4 | 3 | 1 | 8 | 7 | 9 | 6 | 2 |
7 | 1 | 8 | 2 | 9 | 6 | 5 | 4 | 3 |
2 | 9 | 6 | 4 | 3 | 5 | 8 | 7 | 1 |
4 | 7 | 1 | 6 | 5 | 8 | 2 | 3 | 9 |
8 | 3 | 9 | 7 | 2 | 4 | 6 | 1 | 5 |
6 | 5 | 2 | 3 | 1 | 9 | 7 | 8 | 4 |
The first row (3 8 5 9 6 1 4 2 7) contains all digits from 1 to 9. Also the first column (3 9 1 5 7 2 4 8 6) contains all digits from 1 to 9, as does the first subblock (3 8 5 - 9 2 4 - 1 6 7) and all the other ones.
Creating the widget
There are two ways to create Jupyter widgets - an easy way and a more complicated way. For this post, we will be using the easy way, which involves creating two cells in a Jupyter notebook. The first cell contains the Python code for the back-end of the widget, while the second cell contains the JavaScript for the front-end.
If you want to create a proper Python package that can be installed with pip install
, you can follow the more complicated way. A good resource is this tutorial.
For this post, we’ll stick with the easy way.
The Python back-end
The following code defines a Python class named Sudoku
that extends the DOMWidget
class from the ipywidgets
library.
This Sudoku class has the following attributes:
value
: A list of integers that represents the current state of the puzzle.fixed
: A list of booleans that indicates whether a value is part of the original puzzle and cannot be changed by the user.disabled
: A boolean that enables or disables user changes to the puzzle.
The fixed
and value
attributes are defined using the List
trait from the traitlets
library. The validate
decorator is used to define a validator for the value
attribute that checks that all elements are numbers from 0 to 9.
The __getitem__
and __setitem__
methods are implemented to allow indexing and assignment of elements in the value
attribute.
The @register
decorator registers the Sudoku
class as an ipywidget
, which allows it to be displayed and interacted with in a Jupyter environment.
The JavaScript front-end
The front-end contains a bit more code.
%%javascript
'sudoku_widget');
require.undef(
'sudoku_widget', ["@jupyter-widgets/base"], function(widgets) {
define(
// Define the SudokuView
class SudokuView extends widgets.DOMWidgetView {
// Render the view.
render() {= document.createElement('table');
this.sudoku_table = 'collapse';
this.sudoku_table.style.borderCollapse = '0';
this.sudoku_table.style.marginLeft
for (let i=0; i<3; i++) {
= document.createElement('colgroup');
let colgroup = 'solid medium';
colgroup.style.border for (let j=0; j<3; j++) {
= document.createElement('col');
let col = 'solid thin';
col.style.border = '2em';
col.style.width ;
colgroup.appendChild(col)
};
this.sudoku_table.appendChild(colgroup)
}
for (let t=0; t<3; t++) {
= document.createElement('tbody');
let tbody = 'solid medium';
tbody.style.border for (let r=0; r<3; r++) {
= document.createElement('tr');
let tr = '2em';
tr.style.height = 'solid thin';
tr.style.border for (let c=0; c<9; c++) {
= document.createElement('td');
let td ;
tr.appendChild(td)
};
tbody.appendChild(tr)
};
this.sudoku_table.appendChild(tbody)
}
;
this.el.appendChild(this.sudoku_table)
;
this.model_changed()
// Python -> JavaScript update
'change', this.model_changed, this);
this.model.on(
}
model_changed() {= this.sudoku_table.getElementsByTagName('td');
let tds = this.model.get('disabled');
let disabled
for (let i=0; i < 81; i++) {
= tds[i];
let td = ''; // Delete td contents
td.innerText = 'center';
td.style.textAlign = '2em';
td.style.height = this.model.get('_value')[i];
let value = this.model.get('fixed')[i];
let fixed
if (fixed && value > 0) {
= document.createElement('b');
let b = value;
b.innerText ;
td.appendChild(b)else if (disabled && value > 0) {
} = value;
td.innerText else if (!disabled && !fixed) {
} input = document.createElement('input');
let input.type = 'text';
input.maxLength = 1;
input.style.top = 0;
input.style.left = 0;
input.style.margin = 0;
input.style.height = '100%';
input.style.width = '100%';
input.style.border = 'none';
input.style.textAlign = 'center';
input.style.marginTop = 0;
input.style.padding = 0;
input.value = (value > 0 ? value : '');
input.oninput = this.input_input.bind(this, i);
input.onchange = this.input_changed.bind(this, i); // JavaScript -> Python update
input);
td.appendChild(
}
}
}
input_input(i) {'td')[i].getElementsByTagName('input')[0].value =
this.sudoku_table.getElementsByTagName('td')[i].
this.sudoku_table.getElementsByTagName('input')[0].value.replace(/[^1-9]/g,'');
getElementsByTagName(
}
input_changed(i) {'td')[i].getElementsByTagName('input')[0].value =
this.sudoku_table.getElementsByTagName('td')[i].
this.sudoku_table.getElementsByTagName('input')[0].value.replace(/[^1-9]/g,'');
getElementsByTagName(= parseInt(this.sudoku_table.getElementsByTagName('td')[i].getElementsByTagName('input')[0].value) || 0;
let v = this.model.get('_value').slice();
let value = v;
value[i] set('_value', value);
this.model.;
this.model.save_changes()
}
}
return {
SudokuView: SudokuView
}
; })
The define
function defines the sudoku_widget
module, which depends on the @jupyter-widgets/base
module. It creates a SudokuView
class that extends the base class widgets.DOMWidgetView
, which is responsible for rendering and updating the widget.
The render
method of the SudokuView
class creates a table element with 9 rows and 9 columns, representing the Sudoku game board. It adds the table to the widget’s HTML element, and registers a listener for model changes. The model_changed
method is called when the model changes, and it updates the widget’s HTML to reflect the new model state.
The input_input
and input_changed
methods are event handlers that respond to user input on the Sudoku board. They update the model and the widget’s HTML to reflect the new user input.
How to use this widget
Once we have executed these two cells, we’re good to use our widget.
import ipywidgets as widgets
= [
puzzle 0,8,5, 0,6,1, 0,0,0,
9,0,4, 0,0,0, 0,0,0,
0,0,0, 0,0,2, 3,0,8,
0,4,0, 0,0,0, 0,0,2,
7,0,0, 0,9,0, 5,0,0,
0,0,0, 0,3,0, 8,0,0,
0,0,0, 0,5,8, 0,0,0,
0,0,0, 7,0,0, 0,1,0,
6,0,0, 0,0,0, 0,0,4]
= [v > 0 for v in puzzle]
fixed_digits
= Sudoku(value=puzzle, fixed=fixed_digits, disabled=False)
sudoku
display(sudoku)
The widget accepts three parameters: value
, fixed
and disabled
. The parameter value
is a list of digits. A digit of 0 means empty. The parameter fixed
is a list of boolean values, where True
means a digit can’t be edited and will be printed in bold. The boolean disabled
indicates whether a user can edit digits.
One can read the values in a grid like this:
print(sudoku.value)
[0, 8, 5, 0, 6, 1, 0, 0, 0, 9, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 8, 0, 4, 0, 0, 0, 0, 0, 0, 2, 7, 0, 0, 0, 9, 0, 5, 0, 0, 0, 0, 0, 0, 3, 0, 8, 0, 0, 0, 0, 0, 0, 5, 8, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 1, 0, 6, 0, 0, 0, 0, 0, 0, 0, 4]
Running the next cell would show the solution by updating the widget.
= [
solution 3,8,5, 9,6,1, 4,2,7,
9,2,4, 8,7,3, 1,5,6,
1,6,7, 5,4,2, 3,9,8,
5,4,3, 1,8,7, 9,6,2,
7,1,8, 2,9,6, 5,4,3,
2,9,6, 4,3,5, 8,7,1,
4,7,1, 6,5,8, 2,3,9,
8,3,9, 7,2,4, 6,1,5,
6,5,2, 3,1,9, 7,8,4]
= solution sudoku.value
Creating a Sudoku solver
Now that we have this widget to our disposal, we’ll create a Sudoku solver.
Building the user interface
First, let’s tackle the easy part: creating the user interface for our Sudoku solver. We’ll use the Sudoku widget along with some other widgets to make it easy for the user to select from pre-made puzzles or enter their own.
= [
puzzle1 0,8,5, 0,6,1, 0,0,0,
9,0,4, 0,0,0, 0,0,0,
0,0,0, 0,0,2, 3,0,8,
0,4,0, 0,0,0, 0,0,2,
7,0,0, 0,9,0, 5,0,0,
0,0,0, 0,3,0, 8,0,0,
0,0,0, 0,5,8, 0,0,0,
0,0,0, 7,0,0, 0,1,0,
6,0,0, 0,0,0, 0,0,4]
= [
puzzle2 3,6,0, 0,0,0, 0,0,5,
0,1,0, 0,9,0, 2,0,8,
0,5,0, 1,8,0, 0,0,7,
5,0,0, 0,0,6, 4,0,0,
2,4,6, 0,5,0, 7,0,0,
0,0,0, 0,7,0, 0,0,0,
0,0,0, 0,0,7, 1,0,3,
0,0,3, 9,4,0, 0,0,0,
0,0,0, 0,0,1, 0,0,0]
= [
puzzle3 0,2,0, 0,4,0, 0,0,5,
0,5,8, 0,0,0, 0,0,0,
0,1,0, 8,0,0, 4,0,0,
7,0,0, 0,0,8, 0,4,0,
0,0,1, 9,0,5, 7,0,0,
0,3,0, 7,0,0, 0,0,2,
0,0,4, 0,0,3, 0,1,0,
0,0,0, 0,0,0, 9,6,0,
2,0,0, 0,1,0, 0,5,0
]
= Sudoku(value=puzzle1, fixed=[v > 0 for v in puzzle1], disabled=False)
sudoku = widgets.Dropdown(
example_dropdown =[('Empty', [0] * 81), ('Example 1', puzzle1), ('Example 2', puzzle2), ('Example 3', puzzle3)],
options=puzzle1,
value=widgets.Layout(margin='10px 0px 0px 20px', width='150px')
layout
)= widgets.Button(
solve_button ="Solve",
description=widgets.Layout(margin='20px 0px 0px 20px', width='150px')
layout
)= widgets.Button(
next_button ="Next",
description=widgets.Layout(margin='20px 0px 0px 20px', width='150px', display='none')
layout
)= widgets.VBox([example_dropdown, solve_button, next_button])
vbox = widgets.HBox([sudoku, vbox])
hbox = widgets.Label() label
The Sudoku
widget displays a Sudoku board.
There is also a Dropdown
widget for selecting pre-made puzzles or an empty board, and two Button
widgets for solving the puzzle and showing the next solution (if there are multiple solutions).
Finally, there is a Label
widget that can be used to display messages to the user. All of these widgets are arranged in a layout using VBox
and HBox
widgets.
Writing the event handlers
The widgets are not functional on their own; we need to write code to make them responsive to user input.
# global variables
= None
gen = None
solution
def on_example_dropdown_change(change):
if change['type'] == 'change' and change['name'] == 'value':
= change['new']
value = [v > 0 for v in value]
fixed = value
sudoku.value = fixed
sudoku.fixed = ""
label.value = 'inline-block'
solve_button.layout.display = 'none'
next_button.layout.display
example_dropdown.observe(on_example_dropdown_change)
def on_solve_button_clicked(b):
global gen
global solution
= sudoku.value.copy()
val = [v > 0 for v in val]
sudoku.fixed = solve_sudoku(val)
gen try:
= next(gen)
solution = solution
sudoku.value except StopIteration:
= "This sudoku has no solution."
label.value = [False] * 81
sudoku.fixed return
try:
= next(gen).copy()
solution = "This sudoku has multiple solutions."
label.value = 'none'
solve_button.layout.display = 'inline-block'
next_button.layout.display except StopIteration:
= ""
label.value = 'none'
solve_button.layout.display
solve_button.on_click(on_solve_button_clicked)
def on_next_button_clicked(b):
global gen
global solution
= solution
sudoku.value try:
= next(gen)
solution except StopIteration:
= ""
label.value = 'none'
next_button.layout.display
next_button.on_click(on_next_button_clicked)
The on_example_dropdown_change
function is called when the user selects an example puzzle from a dropdown menu, and it sets up the Sudoku grid with the selected puzzle and clears any previous solutions.
The on_solve_button_clicked
function is called when the user clicks a button to solve the puzzle, and it generates a Sudoku solver object and attempts to find a solution to the puzzle. If a solution is found, it updates the Sudoku grid with the solution and enables a button to find the next solution if there are multiple solutions. If no solution is found, it displays an error message.
The on_next_button_clicked
function is called when the user clicks the “next” button to find the next solution to a puzzle with multiple solutions, and it updates the Sudoku grid with the next solution if there is one, or disables the “next” button if there are no more solutions.
The gen
and solution
variables are used to keep track of the state of the Sudoku solver object and the next solution.
The solver
We can easily solve puzzles using backtracking. The solve_sudoku
function utilizes recursion to generate solutions. It is called by the on_solve_button_clicked
function above.
def solve_sudoku(puzzle, index=0):
if index == 81:
# Solution found
yield puzzle
elif puzzle[index] > 0:
# Already filled
yield from solve_sudoku(puzzle, index + 1)
else:
for v in range(1,10):
# Fill in a digit and check constraints
= v
puzzle[index] if is_valid_square(puzzle, index):
yield from solve_sudoku(puzzle, index + 1)
= 0 puzzle[index]
The solve_sudoku
function takes in a puzzle parameter which is a list of length 81, representing the 9x9 Sudoku grid with empty squares represented as 0s. The function yields solutions as they are found.
The following functions are used to check the constraints.
def get_column(puzzle, k):
= []
column for i in range(9):
*9 + k])
column.append(puzzle[ireturn column
def get_row(puzzle, r):
return puzzle[r*9:(r+1)*9]
def get_block(puzzle, b):
= []
block for r in range(3):
for k in range(3):
0,3,6,27,30,33,54,57,60][b]+9*r+k])
block.append(puzzle[[return block
def is_valid(l):
# Check for duplicate values
= [v for v in l if v > 0]
digits = set(digits)
s return len(digits) == len(s)
def is_valid_square(puzzle, i):
= i % 9
k = int(i / 9)
r = int(r / 3) * 3 + int(k / 3)
b
return is_valid(get_row(puzzle, r)) and is_valid(get_column(puzzle, k)) and is_valid(get_block(puzzle, b))
The get_column
, get_row
, and get_block
functions are used to retrieve the values in the columns, rows, and 3x3 blocks that a given index belongs to.
The is_valid
function checks if a list of values contains duplicate values. It returns True
if the list contains no duplicates (excluding 0s) and False
otherwise.
The is_valid_square
function checks if a value can be placed in a given square of the Sudoku grid without violating the rules of the game. It uses the get_row
, get_column
, get_block
, and is_valid
functions.
Displaying the user interface
It’s time to show the user interface.
display(hbox, label)
To play with an interactive version, you’ll need to run it in Jupyter Notebook. Sadly, it won’t work in JupyterLab. It does work in Voilà in case you wish to turn it into a web app. The corresponding gist can be found here.
Conclusion
In this post, we saw how to create a custom widget in Jupyter Notebook. It’s worth noting that the approach I presented here is more of a quick fix. Originally, I developed this code as part of a Sudoku programming assignment for my students, and I had control over the environment they were using (Jupyter Notebook and Voila).
For creating Jupyter widgets, it is recommended to use widget-cookiecutter (for JavaScript) or widget-ts-cookiecutter (for TypeScript). These tools offer a more robust and reliable approach to building widgets in Jupyter.