################################################################################# autoreload all modules every time before executing the Python code%reload_ext autoreload%autoreload 2################################################################################from IPython.core.interactiveshell import InteractiveShell# `ast_node_interactivity` is a setting that determines how the return value of the last line in a cell is displayed# with `last_expr_or_assign`, the return value of the last expression is displayed unless it is assigned to a variableInteractiveShell.ast_node_interactivity ="last_expr_or_assign"################################################################################import pandas as pd# `copy_on_write` is a performance improvement# This will be the default in a future version of pandas# Refer to https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.htmlpd.options.mode.copy_on_write =Truepd.options.future.no_silent_downcasting =True################################################################################%matplotlib inlineimport matplotlib as mplmpl.use("agg")# `constrained_layout` helps avoid overlapping elements# Refer to https://matplotlib.org/stable/tutorials/intermediate/constrainedlayout_guide.htmlmpl.pyplot.rcParams["figure.constrained_layout.use"] =True
The UI elements in this post are not interactive as this is a static page. To see the interactive elements, please run the notebook in a Jupyter environment.
Code
import holoviews as hvimport hvplot.pandas # noqaimport pandas as pdimport panel as pnimport parampn.extension("tabulator")hv.extension("bokeh")
panel is a library that allows creating interactive dashboards in pure Python. It’s a flexible library that allows interlinking matplotlib, bokeh, widgets, and more.
There are however many ways to use panel, some of which are better for certain use cases. I wanted to write this post to share the way I use panel.
Development Constraints
My first constraint when using panel was that I wanted to develop the dashboard in an interactive manner, preferably using a Jupyter notebook environment. panel does allow starting a server using the panel serve --autoreload command and you can pass in the path to a file or a jupyter notebook. However, I want to prototype individual components in a Jupyter notebook, but once I’m happy with the components, I want to combine them into a dashboard. With multiple components like this, using global variables gets unwieldy. Additionally, for multi-page dashboards, I don’t want to have to store different components in different files and run multiple servers.
My second constraint was that I wanted to make the dashboard as modular as possible. I wanted to be able to reuse some components across different dashboards. For example, I might have a component that shows a line chart and I want to use that line chart in a user guide page as well as in the home page.
Lastly, I wanted the code to be usable in a Python script in case someone wanted to programmatically access the same features. Imagine if a user wanted to plot the line chart from the dashboard and annotate it with some custom text. I wanted to make it easy for an advanced user that wanted to do that to have the option to do so. This also means that I wanted to make it easy to test the components in a CI/CD pipeline.
The most natural way to do this was to make the components as classes and then instantiate them as part of the dashboard. This also requires the state and the view to be separate. In this post we are going to talk specifically about that approach. We’ll use the IMDb movies dataset as an example to build a dashboard.
Jupyter prelude
When working in a Jupyter notebook, autoreloading modules is a feature that can be very useful. This means you can make a python package and import the package in the notebook, prototype components in the notebook and move the code over to the package when you are happy with the component. With autoreloading, you don’t have to restart the jupyter notebook kernel every time you make a change to the source code.
# autoreload all modules every time before executing the Python code%reload_ext autoreload%autoreload 2
For this example, I’ve made a package called movies_dashboard that has the following structure:
Another setting that you can enable is last_expr_or_assign. This makes it such that even if the last line of a cell is an assignment, the value of the assignment is displayed.
from IPython.core.interactiveshell import InteractiveShellInteractiveShell.ast_node_interactivity ="last_expr_or_assign"
With this setting, you don’t have to repeat the name of the variable at the end of every cell to see the value of the variable.
import pandas as pddf = pd.read_csv("./data/title.basics.tsv.gz", sep="\t", nrows=500).head()# df ## this line is not required to see the value of df
tconst
titleType
primaryTitle
originalTitle
isAdult
startYear
endYear
runtimeMinutes
genres
0
tt0000001
short
Carmencita
Carmencita
0
1894
\N
1
Documentary,Short
1
tt0000002
short
Le clown et ses chiens
Le clown et ses chiens
0
1892
\N
5
Animation,Short
2
tt0000003
short
Poor Pierrot
Pauvre Pierrot
0
1892
\N
5
Animation,Comedy,Romance
3
tt0000004
short
Un bon bock
Un bon bock
0
1892
\N
12
Animation,Short
4
tt0000005
short
Blacksmith Scene
Blacksmith Scene
0
1893
\N
1
Short
Param State
panel is built on top of param. One useful way to think about these two packages is that param is a way to define state and panel is a way to visualize that state. And making state driven components is a great way to make interactive dashboards.
With param, you can define the state using a class based approach:
import paramclass MoviesStateExample(param.Parameterized): name = param.String() year = param.Integer()m = MoviesStateExample(name ="Goodfellas", year =1990)
MoviesStateExample(name='Goodfellas', year=1990)
And with panel, you can visualize the state:
import panel as pnpn.Param(m)
When making any UI it is important to identify the state variables of a component. This usually involves understanding a few different things:
What are the inputs to the component?
What are the derived properties of the component?
What are the outputs of the component?
This typically forms a unidirectional graph where the inputs are used to derive the properties and the properties are used to derive the outputs.
Identifying Inputs
Let’s say we want to filter based on the year, the average ratings and the runtime of movies. In this case, we would have one variable for the input dataframe; and variables for the year, ratings and runtime ranges.
In this example, when the class is initialized, we may want to load the CSV files, preprocess and clean the data. When self.df = df is called, param will trigger an action with the name of the parameter. And any functions that are listening to that action will be called. We can use this feature to update any derived properties.
There are a few different ways to listen to changes in a parameter.
Add a member function with the param.depends decorator.
@param.depends decorator with watch=True is the simplest because it is more explicit and easier to read.
In all cases, when you use watch=True, you have created a dependent function, and any properties that are updated in that function are dependent properties.
Outputs
Finally, it is important to define the outputs of the component. In this case, the outputs is a derived component that represents the filtered dataframe.
Returning the outputs from a function allows for easy access to the outputs for testing, debugging and for use in other components.
On some occassions however, you might want to store the outputs as a property to cache outputs. This is useful when the output is expensive to compute. In the example below, the output is stored in a filtered_df property and is updated whenever the df, start_year or end_year changes.
One thing to consider is that some dependent properties may computed only as part of the internal state of a component. These dependent properties are usually private variables and are not meant to be accessed directly.
You can signal that a property is private by prefixing the property with an underscore. This is a convention in Python to signal that a property is private and should not be accessed directly.
After setting up the state of an component, we can create a view that presents the state, derived values and the outputs.
One convention is to make this part of a panel() or a view() method that returns the layout and initializes any pn.widgets in, and only in, this method.
When a user updates the start year or the end year, the filtered dataframe is updated. And when that filtered data is updated, the Tabulator widget is updated. This is a very simple example, but this pattern can be extended to more complex dashboards.
It is possible to also derive from pn.viewable.Viewer and implement the __panel__ method. Doing this will return the layout of the component when the component is displayed in a notebook.
Every widget in panel accepts a few different types of arguments (See this section in the official documentation for more information). But the two most common types are:
A Parameter instance
A method using pn.bind or pn.depends that returns a value
1. A parameter instance
It is important to understand the difference between an attribute instance and a parameter instance. An attribute instance is the value that most people typically deal with.
m.start_year
1892
A parameter instance on the other hand is a special instance created by any class that inherits from param.Parameterized.
m.param.start_year
<param.parameters.Integer at 0x317027400>
isinstance(m.param.start_year, param.Parameter)
True
Attribute instances are implemented using descriptors. When the Python interpreter sees m.start_year, it will return the value of the attribute by called the __get__ method of the param.Integer descriptor. This returns the current state at the time of the invocation.
type(m.start_year)
int
m.param.start_year on the other hand is a special value that contains a pm.Parameter instance that allows enabling the reactive features. This special value has to be passed into a widget to create a two way binding, and this has to be done using the from_param method.
And calling that function will return the value of the parameter at that time.
f = pn.bind(_get_dataframe, m.param.filtered_df);f()
primaryTitle
startYear
runtimeMinutes
averageRating
numVotes
0
Carmencita
1894
1
5.7
2115
1
Le clown et ses chiens
1892
5
5.6
285
2
Poor Pierrot
1892
5
6.4
2148
3
Un bon bock
1892
12
5.3
183
4
Blacksmith Scene
1893
1
6.2
2872
But in Jupyter notebooks, even displaying the function will call the function and return the value.
f
b. Using @pn.depends
The input to a widget can also be a function or a method that is part of the class that uses the @pn.depends decorator. If this decorator is used, and when this method is passed to a widget, the method will be called whenever the parameter changes and the return value is used to update the UI.
In this case however, displaying the method will not call the method:
When using @pn.depends, the variable (e.g. _get_dataframe) that would have contained a reference to the function is replaced by a new wrapped function which is reactive. So in this case, you can pass the variable directly to the widget.
Notice that the @pn.depends decorator is used to define the method _get_dataframe does not use the watch=True argument.
If the @pn.depends decorator uses the watch=True argument, the method will be called whenever the parameter changes AND the method will be called again when the widget needs to be updated.
If you are ever confused about this, add print or log statements to the method to see when a method is called.
When the pn.pane.Str is displayed again after the input is changed
but Unwatched is printed only once:
When the pn.pane.Str is displayed again after the input is changed
When watch=True is used, it is recommended to not return anything from that method and simply update the state of the class instead. It’s easier to reason about the code and forces using from_param to bind to a derived property instead.
Example: Movies Dashboard
With just the @param.depends decorator, you can make any reactive component using panel by using two way bindings to update the state and the UI, and trigger updates to other dependencies which in turn can update other parts of the UI.
As a more involved example, I’ve attached a package with a class called Movies that has the following state, derived properties and outputs:
The only real disadvantage of this package based approach is that it is not straightforward to make a wasm only version of the panel dashboard, since that requires it to be all in one file.
This approach allows making a package, using param to define dependent state, create UI when required in a class based approach. In my opinion, this lends itself to better testing, documentation and maintainability in the long run. This is also the approach recommended in the intermediate sections of the panel tutorials. Check out these links for more information:
I also wanted to shout out the panel and param developers for creating such a flexible and powerful library. And thanks to the members of the holoviz community for offering feedback and suggestions on this post.
@online{krishnamurthy2025,
author = {Krishnamurthy, Dheepak},
title = {Building {Dashboards} Using {Param} and {Panel} in {Python}},
date = {2025-01-05},
url = {https://kdheepak.com/blog/building-dashboards-using-param-and-panel-in-python/},
langid = {en}
}