ParamNB is a small library that represents Parameters graphically in a Jupyter notebook. Parameters are Python attributes extended using the Param library to support types, ranges, and documentation, which turns out to be just the information you need to automatically create widgets for each parameter. ParamNB currently uses ipywidgets to display the widgets, but the design of Param and ParamNB allows your code to be completely independent of the underlying widgets library, and ParamNB can be updated to use other widget libraries as they are developed without needing changes in your code.

Parameters and widgets

To use ParamNB, first declare some Parameterized classes with various Parameters:


In [ ]:
import param
import datetime as dt

def hello(x, **kwargs):
    print("Hello %s" % x)
    
class BaseClass(param.Parameterized):
    x                       = param.Parameter(default=3.14,doc="X position")
    y                       = param.Parameter(default="Not editable",constant=True)
    string_value            = param.String(default="str",doc="A string")
    num_int                 = param.Integer(50000,bounds=(-200,100000))
    unbounded_int           = param.Integer(23)
    float_with_hard_bounds  = param.Number(8.2,bounds=(7.5,10))
    float_with_soft_bounds  = param.Number(0.5,bounds=(0,None),softbounds=(0,2))
    unbounded_float         = param.Number(30.01,precedence=0)
    hidden_parameter        = param.Number(2.718,precedence=-1)
    integer_range           = param.Range(default=(3,7),bounds=(0, 10))
    float_range             = param.Range(default=(0,1.57),bounds=(0, 3.145))
    dictionary              = param.Dict(default={"a":2, "b":9})
    
class Example(BaseClass):
    """An example Parameterized class"""
    boolean                 = param.Boolean(True, doc="A sample Boolean parameter")
    color                   = param.Color(default='#FFFFFF')
    date                    = param.Date(dt.datetime(2017, 1, 1),
                                         bounds=(dt.datetime(2017, 1, 1), dt.datetime(2017, 2, 1)))
    select_string           = param.ObjectSelector(default="yellow",objects=["red","yellow","green"])
    select_fn               = param.ObjectSelector(default=list,objects=[list,set,dict])
    int_list                = param.ListSelector(default=[3,5], objects=[1,3,5,7,9],precedence=0.5)
    single_file             = param.FileSelector(path='../*/*.py*',precedence=0.5)
    multiple_files          = param.MultiFileSelector(path='../*/*.py?',precedence=0.5)
    msg                     = param.Action(hello, doc="""Print a message.""",precedence=0.7)
    
Example.num_int

As you can see, declaring Parameters depends only on the separate Param library. Parameters are a simple idea with some properties that are crucial for helping you create clean, usable code:

  • The Param library is pure Python with no dependencies, which makes it easy to include in any code without tying it to a particular GUI or widgets library, or even to the Jupyter notebook.
  • Parameter declarations focus on semantic information relevant to your domain, allowing you to avoid polluting your domain-specific code with anything that ties it to a particular way of displaying or interacting with it.
  • Parameters can be defined wherever they make sense in your inheritance hierarchy, allowing you to document, type, and range-limit them once, with all of those properties inherited by any base class. E.g. parameters work the same here whether they were declared in BaseClass or Example, which makes it easy to provide this metadata once, and avoiding duplicating it throughout the code wherever ranges or types need checking or documentation needs to be stored.

If you then decide to use these Parameterized classes in a notebook environment, you can import ParamNB and easily display and edit the parameter values as an optional additional step:


In [ ]:
import paramnb
paramnb.Widgets(Example)

As you can see, paramnb.Widgets() does not need to be provided with any knowledge of your domain-specific application, not even the names of your parameters; it simply displays widgets for whatever Parameters may have been defined on that object. Using Param with ParamNB thus achieves a nearly complete separation between your domain-specific code and your display code, making it vastly easier to maintain both of them over time. Here even the msg button behavior was specified declaratively, as an action that can be invoked (printing "Hello") independently of whether it is used in a GUI or other context.

Interacting with the widgets above is only supported on a live Python-backed server, but you can also export static renderings of the widgets to a file or web page.

By default, editing values in this way requires you to run the notebook cell by cell -- when you get to the above cell, edit the values as you like, and then move on to execute subsequent cells, where any reference to those parameter values will use your interactively selected setting:


In [ ]:
Example.unbounded_int

In [ ]:
Example.num_int

In [ ]:
#Example.print_param_defaults() # see all parameter values

As you can see, you can access the parameter values at the class level from within the notebook to control behavior explicitly, e.g. to select what to show in subsequent cells. Moreover, any instances of the Parameterized classes in your own code will now use the new parameter values unless specifically overridden in that instance, so you can now import and use your domain-specific library however you like, knowing that it will use your interactive selections wherever those classes appear. None of the domain-specific code needs to know or care that you used ParamNB; it will simply see new values for whatever attributes were changed interactively. ParamNB thus allows you to provide notebook-specific, domain-specific interactive functionality without ever tying your domain-specific code to the notebook environment.

Controlling code execution

If you do Run All in the notebook instead of running cell by cell, you won't get any opportunity to interact with the widgets until the notebook has completed, and so any values you change will only take effect if you then do a separate Run All Below command to update the results of subsequent cells.

Having to work cell by cell or re-run the notebook manually can be awkward, especially when building dashboards that hide the notebook user interface (such as with Jupyter Dashboards). In order to provide "live" or dynamic updating, ParamNB also allows you to control code (re-)execution automatically in various ways. First, you can define what code will be executed:

  • callback=callable: User-defined function to call, if any
  • next_n=n: zero by default, but if set to e.g. 2, will execute the subsequent 2 cells of the notebook

You can also define when the code will be executed:

  • button=False: the default; the specified code will be executed whenever a widget value is changed
  • button=True: Provide a button to control code execution, so that multiple widgets can be adjusted and code is updated only when the button is pushed.
  • continuous_update=True: the specified code is executed for every movement of a slider
  • continuous_update=False: the default; the specified code is executed only once a widget has been released

These options allow you to choose between various levels of dynamic interactivity, as appropriate for the computational and semantic requirements of the code you are executing. Rough guidelines:

  • button=False,continuous_update=True: Provides a smooth, dynamic user experience, with text or plots updating immediately as a slider is dragged. Appropriate only for inexpensive operations, where rexecuting the code multiple times on the fly is not an issue.
  • button=False,continuous_update=False: The default; a good middle ground appropriate for most interactive use, with relatively responsive interactivity, updating each time a widget is released. Suitable for relatively expensive operations, but not so expensive that it is problematic to have them run once for each widget adjusted.
  • button=True: Suitable for very expensive or transactional operations, where you want to adjust multiple widgets before committing to executing the code.

Example of dynamic updating:


In [ ]:
class Example2(param.Parameterized):
    num1 = param.Number(3.14,bounds=(0.0,10.0))
    number2 = param.Integer(2,bounds=(0,5))

paramnb.Widgets(Example2,next_n=1)

In [ ]:
Example2.num1, Example2.number2

Notice that in a live notebook, the In and Out numbers of the above cell increase every time you release a slider after dragging it, because that cell is being re-executed.

Example of updating on the "Run" button press:


In [ ]:
paramnb.Widgets(Example2,button=True,callback=hello,next_n=1)

In [ ]:
Example2.num1, Example2.number2

Here, the cell above changes its number (and output value) only when the "Run 1" button is pressed in the previous cell. The supplied callback is also executed at that time.

Note that paramnb.Widgets() displays all the parameters that have a precedence that's above the Widgets.display_threshold value, which is zero by default. You can thus hide values that are not useful in the notebook by giving the parameters a negative precedence when they are declared. If you later want to display the hidden parameters, e.g. for debugging, you can change the display_threshold parameter, e.g. by supplying it to the Widgets() call. Parameters with the same precedence are sorted alphanumerically, in groups sorted by the precedence value. Values with no declared precedence are given a very low precedence by default (Widgets(...,default_precedence=1e-8)), allowing you to force parameters to appear at the top of the list by giving them a precedence of zero (or another very small number).

Together, all these features make it simple to add interactive controls in Jupyter notebooks: just declare your parameters wherever their values will need to be used, using the Param library (pure Python, zero dependencies), then add an optional Widgets() declaration in your notebook wherever you want to be able to modify those values interactively. That way your main code can be fully independent of any GUI or notebook display, while your notebooks can easily expose the parameters declared in your main code, without duplicating their names or definitions and without relying on any specific details of that code. So you can now have full interactivity without tying yourself to any particular user interface or GUI library, and without tying your user interface code to details of your domain-specific code.

You can install ParamNB as described at github.com/ioam/paramnb. Have fun widgeting!