Build an app#

At this point you should have set up your environment and installed Panel and been introduced some of the basic ideas and APIs behind Panel in the Core Concepts guide.

However the easiest way to get started with Panel is to play around with it yourself. In this section we’re going to be building a basic application using a public dataset and add some interactivity. The easiest way to follow along is to copy the examples and launch a Jupyter notebook session as described in the installation guide.


This guide is written for an interactive environment such as Jupyter notebooks. The interactive widgets will not work in a static version of this documentation.

Fetch some data#

Panel lets you add interactive controls for just about anything you can display in Python. Panel can help you build simple interactive apps, complex multi-page dashboards, or anything in between. As a simple example, let’s say we have loaded the UCI ML dataset measuring the environment in a meeting room:

import pandas as pd; import numpy as np; import matplotlib.pyplot as plt

csv_file = ''
data = pd.read_csv(csv_file, parse_dates=['date'], index_col='date')

Temperature Humidity Light CO2 HumidityRatio Occupancy
2015-02-10 09:29:00 21.05 36.0975 433.0 787.250000 0.005579 1
2015-02-10 09:29:59 21.05 35.9950 433.0 789.500000 0.005563 1
2015-02-10 09:30:59 21.10 36.0950 433.0 798.500000 0.005596 1
2015-02-10 09:32:00 21.10 36.2600 433.0 820.333333 0.005621 1
2015-02-10 09:33:00 21.10 36.2000 447.0 821.000000 0.005612 1

Visualize the data#

And we’ve written some code that smooths a time series and plots it using Matplotlib with outliers highlighted:

from matplotlib.figure import Figure

%matplotlib inline

def mpl_plot(avg, highlight):
    fig = Figure()
    ax = fig.add_subplot()
    if len(highlight): highlight.plot(style='o', ax=ax)
    return fig

def find_outliers(variable='Temperature', window=30, sigma=10, view_fn=mpl_plot):
    avg = data[variable].rolling(window=window).mean()
    residual = data[variable] - avg
    std = residual.rolling(window=window).std()
    outliers = (np.abs(residual) > std * sigma)
    return view_fn(avg, avg[outliers])

We can call the function with parameters and get a plot:

find_outliers(variable='Temperature', window=20, sigma=10)

It works! But exploring all these parameters by typing Python is slow and tedious. Plus we want our boss, or the boss’s boss, to be able to try it out.

If we wanted to try out lots of combinations of these values to understand how the window and sigma affect the plot, we could reevaluate the above cell lots of times, but that would be a slow and painful process, and is only really appropriate for users who are comfortable with editing Python code. In the next few examples we will demonstrate how to use Panel to quickly add some interactive controls to some object and make a simple app.

Reactive functions#

Instead of editing code, it’s much quicker and more straightforward to use sliders to adjust the values interactively. You can easily make a Panel app by binding some widgets to the arguments of a function using pn.bind:

import panel as pn

window = pn.widgets.IntSlider(name='window', value=30, start=1, end=60)
sigma = pn.widgets.IntSlider(name='sigma', value=10, start=0, end=20)

interactive = pn.bind(find_outliers, window=window, sigma=sigma)

Once you have bound the widgets to the function’s arguments you can lay out the component being returned using Panel layout components such as Row, Column, or FlexBox:

first_app = pn.Column(window, sigma, interactive)


As long as you have a live Python process running, dragging these widgets will trigger a call to the find_outliers callback function, evaluating it for whatever combination of parameter values you select and displaying the results. A Panel like this makes it very easy to explore any function that produces a visual result of a supported type, such as Matplotlib (as above), Bokeh, Plotly, Altair, or various text and image types.

Deploying Panels#

The above panels all work in the notebook cell (if you have a live Jupyter kernel running), but unlike other approaches such as ipywidgets, Panel apps work just the same in a standalone server. For instance, the app above can be launched as its own web server on your machine by uncommenting and running the following cell:

Or, you can simply mark whatever you want to be in the separate web page with .servable(), and then run the shell command panel serve --show Create_App.ipynb to launch a server containing that object. (Here, we’ve also added a semicolon to avoid getting another copy of the occupancy app here in the notebook.)


During development, particularly when working with a raw script using panel serve --show --autoreload can be very useful as the application will automatically update whenever the script or notebook or any of its imports change.

Declarative Panels#

The above compositional approach is very flexible, but it ties your domain-specific code (the parts about sine waves) with your widget display code. That’s fine for small, quick projects or projects dominated by visualization code, but what about large-scale, long-lived projects, where the code is used in many different contexts over time, such as in large batch runs, one-off command-line usage, notebooks, and deployed dashboards? For larger projects like that, it’s important to be able to separate the parts of the code that are about the underlying domain (i.e. application or research area) from those that are tied to specific display technologies (such as Jupyter notebooks or web servers).

For such usages, Panel supports objects declared with the separate Param library, which provides a GUI-independent way of capturing and declaring the parameters of your objects (and dependencies between your code and those parameters), in a way that’s independent of any particular application or dashboard technology. For instance, the above code can be captured in an object that declares the ranges and values of all parameters, as well as how to generate the plot, independently of the Panel library or any other way of interacting with the object:

import param

def hvplot(avg, highlight):
    return avg.hvplot(height=200) * highlight.hvplot.scatter(color='orange', padding=0.1)

class RoomOccupancy(param.Parameterized):
    variable  = param.Selector(objects=list(data.columns))
    window    = param.Integer(default=10, bounds=(1, 20))
    sigma     = param.Number(default=10, bounds=(0, 20))

    def view(self):
        return find_outliers(self.variable, self.window, self.sigma, view_fn=hvplot)
obj = RoomOccupancy()
RoomOccupancy(name='RoomOccupancy01388', sigma=10, variable='Temperature', window=10)

The RoomOccupancy class and the obj instance have no dependency on Panel, Jupyter, or any other GUI or web toolkit; they simply declare facts about a certain domain (such as that smoothing requires window and sigma parameters, and that window is an integer greater than 0 and sigma is a positive real number). This information is then enough for Panel to create an editable and viewable representation for this object without having to specify anything that depends on the domain-specific details encapsulated in obj:

pn.Row(obj.param, obj.view)
AttributeError                            Traceback (most recent call last)
Input In [9], in <cell line: 1>()
----> 1 pn.Row(obj.param, obj.view)

File ~/work/panel/panel/panel/layout/, in ListPanel.__init__(self, *objects, **params)
    626     if 'objects' in params:
    627         raise ValueError("A %s's objects should be supplied either "
    628                          "as positional arguments or as a keyword, "
    629                          "not both." % type(self).__name__)
--> 630     params['objects'] = [panel(pane) for pane in objects]
    631 elif 'objects' in params:
    632     params['objects'] = [panel(pane) for pane in params['objects']]

File ~/work/panel/panel/panel/layout/, in <listcomp>(.0)
    626     if 'objects' in params:
    627         raise ValueError("A %s's objects should be supplied either "
    628                          "as positional arguments or as a keyword, "
    629                          "not both." % type(self).__name__)
--> 630     params['objects'] = [panel(pane) for pane in objects]
    631 elif 'objects' in params:
    632     params['objects'] = [panel(pane) for pane in params['objects']]

File ~/work/panel/panel/panel/pane/, in panel(obj, **kwargs)
     80 if kwargs.get('name', False) is None:
     81     kwargs.pop('name')
---> 82 pane = PaneBase.get_pane_type(obj, **kwargs)(obj, **kwargs)
     83 if len(pane.layout) == 1 and pane._unpack:
     84     return pane.layout[0]

File ~/work/panel/panel/panel/, in ParamMethod.__init__(self, object, **params)
    759 if object is not None:
    760     self._validate_object()
--> 761     self._replace_pane(not self.lazy)

File ~/work/panel/panel/panel/, in ParamMethod._replace_pane(self, force, *args)
    808     new_object = Spacer()
    809 else:
--> 810     new_object = self.eval(self.object)
    811 if inspect.isawaitable(new_object):
    812     param.parameterized.async_executor(partial(self._eval_async, new_object))

File ~/work/panel/panel/panel/, in ParamMethod.eval(self, function)
    791         args = (getattr(dep.owner, for dep in arg_deps)
    792         kwargs = {n: getattr(dep.owner, for n, dep in kw_deps.items()}
--> 793 return function(*args, **kwargs)

Input In [8], in RoomOccupancy.view(self)
     11 def view(self):
---> 12     return find_outliers(self.variable, self.window, self.sigma, view_fn=hvplot)

Input In [2], in find_outliers(variable, window, sigma, view_fn)
     15 std = residual.rolling(window=window).std()
     16 outliers = (np.abs(residual) > std * sigma)
---> 17 return view_fn(avg, avg[outliers])

Input In [8], in hvplot(avg, highlight)
      3 def hvplot(avg, highlight):
----> 4     return avg.hvplot(height=200) * highlight.hvplot.scatter(color='orange', padding=0.1)

File /usr/share/miniconda3/envs/test-environment/lib/python3.8/site-packages/pandas/core/, in NDFrame.__getattr__(self, name)
   5568 if (
   5569     name not in self._internal_names_set
   5570     and name not in self._metadata
   5571     and name not in self._accessors
   5572     and self._info_axis._can_hold_identifiers_and_holds_name(name)
   5573 ):
   5574     return self[name]
-> 5575 return object.__getattribute__(self, name)

AttributeError: 'Series' object has no attribute 'hvplot'

To support a particular domain, you can create hierarchies of such classes encapsulating all the parameters and functionality you need across different families of objects, with both parameters and code inheriting across the classes as appropriate, all without any dependency on a particular GUI library or even the presence of a GUI at all. This approach makes it practical to maintain a large codebase, all fully displayable and editable with Panel, in a way that can be maintained and adapted over time.

Exploring further#

For a quick reference of different Panel functionality refer to the overview. If you want a more detailed description of different ways of using Panel, each appropriate for different applications see the following materials:

  • APIs: An overview of the different APIs offered by Panel.

  • Parameters: Capturing parameters and their links to actions declaratively

Just pick the style that seems most appropriate for the task you want to do, then study that section of the user guide. Regardless of which approach you take, you’ll want to learn more about Panel’s panes and layouts:

  • Components: An overview of the core components of Panel including Panes, Widgets and Layouts

  • Customization: How to set styles and sizes of Panel components

  • Deploy & Export: An overview on how to display, export and deploy Panel apps and dashboards

  • Templates: Composing one or more Panel objects into a template with full control over layout and styling.

This web page was generated from a Jupyter notebook and not all interactivity will work on this website. Right click to download and run locally for full Python-backed interactivity.