Panel and Param#

import param
import panel as pn

pn.extension()

Param is a Python package that is foundational in the implementation and usage of Panel, and more generally of the HoloViz tools. Param provides super-charged object attributes, called Parameters, that behave like normal Python object attributes but benefit from two major features that are both heavily used in Panel:

  • Parameter attribute values are runtime validated.

  • Parameters are objects that can be watched, i.e. you can register callbacks (Python functions or methods) that will be executed when the parameter value changes.

Parameterized and Parameter intro#

A Parameterized class, i.e. a class on which Parameters can be set, is created by inheriting from param.Parameterized. Most Panel objects (widgets, panels, layouts, templates, etc.) actually inherit from param.Parameterized!

print(
    issubclass(pn.widgets.FloatSlider, param.Parameterized),
    issubclass(pn.pane.Matplotlib, param.Parameterized),
    issubclass(pn.Column, param.Parameterized),
    issubclass(pn.template.BootstrapTemplate, param.Parameterized)
)
True True True True

Let’s create a Parameterized class with a single Parameter.

class A(param.Parameterized):
    x = param.Number()

A has now an attribute x that can be accessed and set as you would normally do with a plain Python class.

A.x
0.0

As each Parameter type has a default value, we can instantiate A as is.

a = A()
a.x
0.0

Obviously, we can set a new value for x on a.

a.x = 2
a.x
2

In Panel documentation we often refer to the Parameter value to describe the attribute value. The Parameter object, i.e. the object declared in the class body and of type param.Parameter, can be accessed via the .param namespace, both at the class and instance level.

A.param.x  # equivalent to A.param['x']
<param.parameters.Number object at 0x1100c1e40>
a.param.x  # # equivalent to a.param['x']
<param.parameters.Number object at 0x1100c2080>

The Parameter object has methods and attributes that can be useful to interact with. We can for instance access the default value of the x Parameter.

a.param.x.default
0.0

Parameterized classes often don’t need a constructor, because the default constructor already allows them to be configured using Parameters as constructor arguments. Still, sometimes you do need to customize the constructor of a Parameterized class. This happens regularly enough that Param’s users have come up with the following convention that you are likely to encounter.

class A(param.Parameterized):
    x = param.Number()

    # Naming the keyword arguments `**params` is purely a convention.
    def __init__(self, **params):
        super().__init__(**params)
        self._y = self.x * 2

Attention

For an object attribute to be powered by Param you must declare it as a Parameter! Otherwise, that attribute will become a regular class variable, and as such will be shared across all the instances of that class.

class P(param.Parameterized):
    x = param.Number()  # Good
    w1 = pn.widgets.FloatSlider()  # Very likely you DO NOT want this
    w2 = param.ClassSelector(class_=pn.widgets.FloatSlider)  # Much better!

Runtime type-checking#

The class B below is created with 4 Parameters:

  • t is a Number Parameter that only accepts Python int and float values.

  • i is an Integer Parameter that only accepts Python int values and that must be within the interval [5, 15]

  • s is a String Parameter that only accepts Python str values and is documented with doc.

  • option is a Selector Parameter that only accepts one of the values listed in objects.

Note

Param offers many Parameter types. All of them share a common set of arguments like default or doc, and some add other arguments such as bounds for the numerical Parameters.

class B(param.Parameterized):
    t = param.Number()
    i = param.Integer(default=10, bounds=(5, 15))
    s = param.String(default='a string', doc='The simulation name')
    option = param.Selector(objects=['a', 'b', 'c'])

Once the class is created, the Parameters are already active and can be updated, but only if they’re valid!

# Updating `t` to 2 is valid
B.t = 2

# However updating `i` with a string isn't and raises an error
try:
    B.i = 'bad data'
except Exception as e:
    print(e)
Integer parameter 'B.i' must be an integer, not <class 'str'>.

The same principle applies at the instance level, both when creating the instance and when updating it later on.

# Setting `t` to 3 is valid
b = B(t=3)

# However setting `i` to a string isn't and raises an error
try:
    B(i='bad data')
except Exception as e:
    print(e)
Integer parameter 'B.i' must be an integer, not <class 'str'>.
# Setting `s` to a string is valid
b.s = 'foo'

# However setting `option` to a value not found in `objects` isn't
try:
    b.option = 'bad data'
except Exception as e:
    print(e)
Selector parameter 'B.option' does not accept 'bad data'; valid options include: '[a, b, c]'

As you can see, code inside of Parameterized objects and code that uses Parameterized objects can often be written without any special error checking, since it knows that only allowed values will ever be present.

Dependencies and watchers#

Parameters are objects that can be watched, i.e., you can register callbacks that will be triggered when their value changes.

The class C has 3 Parameters. Using the param.depends decorator, we can declare the callbacks of these classes, i.e. its methods, and the Parameters that they depend on:

  • updating_on_t depends on both t1 and t2.

  • updating_on_s deponds on s only.

By setting watch=True (default: False), we let Param know that we want it to trigger the callback automatically whenever the Parameters it depends on are updated. Without setting watch, we’re only declaring the dependency between a callback and its Parameters, which is information that is stored by Param but not used by it otherwise. Libraries like Panel that read this information can then use the declarations to determine their own flow of program execution, as we will see later.

class C(param.Parameterized):
    t1 = param.Number(default=2)
    t2 = param.Number(default=3)
    s = param.String(default='a string')

    @param.depends('t1', 't2', watch=True)
    def updating_on_t(self):
        print(f'New value of t1 and t2: {self.t1}, {self.t2}')

    @param.depends('s')
    def updating_on_s(self):
        print(f'New value of s: {self.s}')

c = C()

Let’s confirm this behavior by setting new values to these Parameters.

c.t1 = 0
New value of t1 and t2: 0, 3
c.t2 = 1
New value of t1 and t2: 0, 1
c.s = 'another string'

Param offers a more low-level API to set up watchers, with the watch function available on the .param namespace. watch accepts a callback and a list of Parameter names, and a few other arguments. The callback will receive an Event object, and the new Parameter value can be found on its new attribute.

def callback(event):
    print(event)
    print()
    print(f'Old value of s: {event.old}')
    print(f'New value of s: {event.new}')

# Setting up a watcher that will trigger the callback when `s` changes.
c.param.watch(callback, ['s'])
Watcher(inst=C(name='C00115', s='another string', t1=0, t2=1), cls=<class 'panel.io.mime_render.C'>, fn=<function callback at 0x1101d7d80>, mode='args', onlychanged=True, parameter_names=('s',), what='value', queued=False, precedence=0)
c.s = 'new string'
Event(what='value', name='s', obj=C(name='C00115', s='new string', t1=0, t2=1), cls=C(name='C00115', s='new string', t1=0, t2=1), old='another string', new='new string', type='changed')

Old value of s: another string
New value of s: new string

Panel and Param#

Panel knows how to map Parameters to widgets, and so it can easily generate a set of widgets from Parameterized class that control its Parameters and trigger any dependent callbacks:

class D(param.Parameterized):
    t = param.Number()
    i = param.Integer(default=10, bounds=(5, 15))
    s = param.String(default='a string', doc='The simulation name')
    option = param.Selector(objects=['a', 'b', 'c'])

    @param.depends('t', 'i')
    def compute(self):
        return self.t * self.i

d = D()

pn.panel(d.param)

Panel, when given a method decorated with @param.depends, will re-run the method and render its new output every time one of the Parameters it depends on change:

pn.Row(d.param.t, d.param.i, d.compute)

Because the displayable objects provided by Panel are all Parameterized objects with their own set of Parameters, they can all be watched. We can for instance hide a widget (setting widget.visible to False) by watching the value of another widget:

checkbox = pn.widgets.Checkbox(value=True)
slider = pn.widgets.FloatSlider()

def hide(event):
    slider.visible = event.new

checkbox.param.watch(hide, 'value')

pn.Row(checkbox, slider)

Using .param.watch as done just above is a valid (albeit pretty verbose!) way to set up some interactivity between Panel components. Panel also provides a much more natural “reactive” API, allowing you to bind the value of two Parameters together, or to bind the value of a Parameter to a callback that depends on some additional Parameters. In the example below, tinput.visible and output.visible will be updated whenever checkbox.value changes (clicking on the checkbox), and the value of output.object will be updated whenever tinput.value changes, that value being transformed by the boldit callback.

checkbox = pn.widgets.Checkbox(value=True)
tinput = pn.widgets.TextInput(value='some text always bold', visible=checkbox.param.value)

def boldit(value):
    return f'**{value}**'

output = pn.pane.Markdown(object=pn.bind(boldit, tinput.param.value), visible=checkbox.param.value)
pn.Row(checkbox, tinput, output)

As you can see, Parameters offer a very generic mechanism for your Python code to declare options that control it, and Panel’s Param support allows you to work naturally with any combination of objects and their Parameters.