Developer Experience#

import time

import param
import panel as pn

pn.extension()

The best practices described on this page serve as a checklist of items to keep in mind as you are developing your application. They include items we see users frequently get confused about or things that are easily missed but can make a big difference to the user experience of your application(s).

Note

  • Good: recommended, works.

  • Okay: works (with intended behavior), potentially inefficient.

  • Bad: Deprecated (may or may not work), just don’t do it.

  • Wrong: Not intended behavior, won’t really work.

Bind on reference value, not value#

Good#

Be sure to bind obj.param.{parameter} (the Parameter object), not just {parameter} (the current Parameter value).

def show_clicks(clicks):
    return f"Number of clicks: {clicks}"

button = pn.widgets.Button(name="Click me!")
clicks = pn.bind(show_clicks, button.param.clicks)
pn.Row(button, clicks)

Wrong#

Binding to {parameter} will bind to the single current value of the parameter, not to the underlying object, and so it will not trigger an update on change.

def show_clicks(clicks):
    return f"Number of clicks: {clicks}"

button = pn.widgets.Button(name="Click me!")
clicks = pn.bind(show_clicks, button.clicks)  # not button.clicks!
pn.Row(button, clicks)
WARNING:param.ParamFunction00132: The function 'show_clicks' does not have any dependencies and will never update. Are you sure you did not intend to depend on or bind a parameter or widget to this function? If not simply call the function before passing it to Panel. Otherwise, when passing a parameter as an argument, ensure you pass at least one parameter and reference the actual parameter object not the current value, i.e. use object.param.parameter not object.parameter.

Inherit from pn.viewer.Viewer or pn.custom.PyComponent#

Good#

param.Parameterized is a very general class that can be used separately from Panel for working with Parameters.

But if you want a Parameterized class to use with Panel, it is usually appropriate to inherit from the Panel-specific class pn.viewable.Viewer instead, because Viewer allows direct instantiation of the Viewer class, resembling a native Panel object.

For example, it’s possible to use ExampleApp().servable() instead of ExampleApp().view().servable().

class ExampleApp(pn.viewable.Viewer):

    ...

    def __panel__(self):
        return pn.template.FastListTemplate(
            main=[...],
            sidebar=[...],
        )

ExampleApp().servable();

Viewer is ideal if you’re building a class with some fairly specific business logic, but if you are building a reusable component assembled from other Panel components use PyComponent instead.

class MultipleChildren(PyComponent):

    objects = Children()

    def __panel__(self):
        return pn.Column(objects=self.param['objects'], styles={"background": "silver"})

Okay#

Inheriting from param.Parameterized also works, but should be reserved for cases where there’s no Panel output.

class ExampleApp(param.Parameterized):

    ...

    def view(self):
        return pn.template.FastListTemplate(
            main=[...],
            sidebar=[...],
        )

ExampleApp().view().servable();

Build widgets from parameters#

Good#

To translate multiple Parameters into widgets, use pn.Param.

class ExampleApp(pn.viewable.Viewer):

    width = param.Integer(default=100, bounds=(1, 200), label="Width of box")
    height = param.Integer(default=100, bounds=(1, 250), label="Height of box")
    color = param.Color(default="red", label="Color of box")

    def __panel__(self):
        return pn.Column(
            pn.Param(self, widgets={"height": pn.widgets.IntInput}),
            pn.pane.HTML(
                width=self.param.width,
                height=self.param.height,
                styles={"background-color": self.param.color},
            ),
        )


ExampleApp()

Good#

You can also use from_param to manually build each component.

class ExampleApp(pn.viewable.Viewer):

    width = param.Integer(default=100, bounds=(1, 200), label="Width of box")
    height = param.Integer(default=100, bounds=(1, 250), label="Height of box")
    color = param.Color(default="red", label="Color of box")

    def __panel__(self):
        width_slider = pn.widgets.IntSlider.from_param(self.param.width)
        height_input = pn.widgets.IntInput.from_param(self.param.height)
        color_picker = pn.widgets.ColorPicker.from_param(self.param.color)
        return pn.Column(
            width_slider,
            height_input,
            color_picker,
            pn.pane.HTML(
                width=self.param.width,
                height=self.param.height,
                styles={"background-color": self.param.color},
            ),
        )


ExampleApp()

Bad#

If you instantiate individually through param, it’s not bidirectional.

class ExampleApp(pn.viewable.Viewer):

    width = param.Integer(default=100, bounds=(1, 200), label="Width of box")
    height = param.Integer(default=100, bounds=(1, 250), label="Height of box")
    color = param.Color(default="red", label="Color of box")

    def __panel__(self):
        width_slider = pn.widgets.IntSlider(
            value=self.param.width,
            start=self.param["width"].bounds[0],
            end=self.param["width"].bounds[1],
            name=self.param["width"].label,
        )
        height_input = pn.widgets.IntInput(
            value=self.param.height,
            start=self.param["height"].bounds[0],
            end=self.param["height"].bounds[1],
            name=self.param["height"].label,
        )
        color_picker = pn.widgets.ColorPicker(
            value=self.param.color,
            name=self.param["color"].label,
            width=200,
        )
        return pn.Column(
            width_slider,
            height_input,
            color_picker,
            pn.pane.HTML(
                width=self.param.width,
                height=self.param.height,
                styles={"background-color": self.param.color},
            ),
        )


ExampleApp()

Bad#

It’s possible to link each widget to self with bidirectional=True, but certain keyword arguments, like bounds, cannot be linked easily.

class ExampleApp(pn.viewable.Viewer):

    width = param.Integer(default=100, bounds=(1, 200), label="Width of box")
    height = param.Integer(default=100, bounds=(1, 250), label="Height of box")
    color = param.Color(default="red", label="Color of box")

    def __panel__(self):
        width_slider = pn.widgets.IntSlider()
        height_input = pn.widgets.IntInput()
        color_picker = pn.widgets.ColorPicker()

        width_slider.link(self, value="width", bidirectional=True)
        height_input.link(self, value="height", bidirectional=True)
        color_picker.link(self, value="color", bidirectional=True)

        return pn.Column(
            width_slider,
            height_input,
            color_picker,
            pn.pane.HTML(
                width=self.param.width,
                height=self.param.height,
                styles={"background-color": self.param.color},
            ),
        )


ExampleApp()

Wrong#

Widgets should not be used as if they were Parameters, because then all instances of your new class will share a single set of instantiated widgets, confusing everyone:

class ExampleApp(pn.viewable.Viewer):

    width = pn.widgets.IntSlider()
    height = pn.widgets.IntInput()
    color = pn.widgets.ColorPicker()

Show templates in notebooks#

Good#

Templates, at the time of writing, are not able to be rendered properly in Jupyter notebooks.

To continue working with templates in notebooks, call show to pop up a new browser window.

template = pn.template.FastListTemplate(
    main=[...],
    sidebar=[...],
)

# template.show()  # commented out to disable opening a new browser tab in example

Okay#

Alternatively, you can use a barebones notebook template like the one below.

class NotebookPlaceholderTemplate(pn.viewable.Viewer):
    main = param.List()
    sidebar = param.List()
    header = param.List()
    title = param.String()

    def __panel__(self):
        title = pn.pane.Markdown(f"# {self.title}", sizing_mode="stretch_width")
        # pastel blue
        header_row = pn.Row(
            title,
            *self.header,
            sizing_mode="stretch_width",
            styles={"background": "#e6f2ff"},
        )
        main_col = pn.WidgetBox(*self.main, sizing_mode="stretch_both")
        sidebar_col = pn.WidgetBox(
            *self.sidebar, width=300, sizing_mode="stretch_height"
        )
        return pn.Column(
            header_row,
            pn.Row(sidebar_col, main_col, sizing_mode="stretch_both"),
            sizing_mode="stretch_both",
            min_height=400,
        )

template = pn.template.FastListTemplate(
    main=[...],
    sidebar=[...],
)

template;

Yield to show intermediate values#

Good#

Use a generator (yield) to provide incremental updates.

def increment_to_value(value):
    for i in range(value):
        time.sleep(0.1)
        yield i

slider = pn.widgets.IntSlider(start=1, end=10)
output = pn.bind(increment_to_value, slider.param.value_throttled)
pn.Row(slider, output)

Watch side effects#

Good#

For functions that trigger side effects, i.e. do not return anything (or return None), be sure to set watch=True on pn.bind or pn.depends.

def print_clicks(clicks):
    print(f"Number of clicks: {clicks}")

button = pn.widgets.Button(name="Click me!")
pn.bind(print_clicks, button.param.clicks, watch=True)
button

Good#

For buttons, you can also use on_click.

def print_clicks(event):
    clicks = event.new
    print(f"Number of clicks: {clicks}")

button = pn.widgets.Button(name="Click me!", on_click=print_clicks)
button

Okay#

For all other widgets, use obj.param.watch() for side effects.

def print_clicks(event):
    clicks = event.new
    print(f"Number of clicks: {clicks}")

button = pn.widgets.Button(name="Click me!")
button.param.watch(print_clicks, "clicks")
button

Refreshing layout objects#

Good#

Updating the objects on a layout should be done via the methods on the layout itself:

def print_objects(event):
    print(f"Got new {[pane.object for pane in event.new]}")

col = pn.Column("a", "b")

col.param.watch(print_objects, 'objects')

col
col[:] = ["c", *col.objects[1:]]
Got new ['c', 'b']

Wrong#

Modifying container objects by index will not trigger the callback.

def print_objects(event):
    print(f"Got new {event.new}")

col = pn.Column("a", "b")

col.param.watch(print_objects, "objects")

col
col.objects[0] = ["c"]  # does not trigger

Good#

However, you can modify the container by index using the APIs on the component itself.

def print_objects(event):
    print(f"Got new {[pane.object for pane in event.new]}")

col = pn.Column("a", "b")

col.param.watch(print_objects, "objects")

col
# col.objects[0] = 'Foo'  # no
col[0] = 'Foo'  # yes
Got new ['Foo', 'b']