Core Concepts#

In the last section we learned how to build a simple app. A Panel app like this makes it very easy to explore any function that produces a visual result of a supported type, such as Matplotlib, Bokeh, Plotly, Altair, or various text and image types.

Development flow#

As you prepare to develop your Panel application, there are a couple initial development decisions to make about API and environment:

  1. Will you be programming with a Python class-based approach, suitable for more complex apps? If not, or if you are unsure, we recommend starting with the reactive API (pn.bind) that you already tasted while building a simple app.

Interested in a class-based approach instead?

Checkout the same ‘outlier’ app built using a class-based declarative API. And to dive deeper, read the Explanation > APIs section for a discussion on each of the API options.

  1. Will you be developing in a notebook or in an editor environment? If you are unsure, we recommend starting in a notebook as you get familiar with Panel, but you can switch between them at any time. Let’s talk a bit more about these development environment options:

Notebook#

In a notebook you can quickly iterate on individual components of your application because Panel components render inline. To get started in a notebook simply import panel and initialize the extension with pn.extension().

import panel as pn
pn.extension()

Now, when you put a Panel component at the end of a notebook cell it will render as part of the output of that cell. By changing the code in your cell and re-running it you can quickly iterate and build the individual units that will make up your final application. This way of working has many benefits because you can work on each component in your application individually without having to re-run your entire application each time.

Editor#

If you are working in an editor, declare the Panel components that you want to display as servable (we will discover what that means soon), then launch the script or notebook file. For example, if you were to save the app you built in the previous page into app.py and declared first_app.servable() within the file, then on the command line you can just run:

panel serve app.py --autoreload --show

Once you run that command Panel will launch a server that will serve your app, open a tab in your default browser (--show) and update the application whenever you update the code (--autoreload).

Checkout How-to > Prepare to develop for more guidance on each of the development environment options.

Control flow#

Now let’s get into how Panel actually works. Panel is built on a library called Param that controls how information flows through your application. When a Parameter changes, e.g. the value of a slider updates or when you update the value manually in code, events are triggered that your app can respond to. Panel provides a number of high-level and lower-level approaches for setting up interactivity in response to updates in Parameters. Understanding some of the basic concepts behind Param is essential to getting the hang of Panel.

Let’s start simple and answer the question “what is Param?”

  • Param is a framework that lets Python classes have attributes with defaults, type/value validation and callbacks when a value changes.

  • Param is similar to other frameworks like Python dataclasses, pydantic, and traitlets

One of the most important concepts to understand in both Param and Panel is the ability to use Parameters as references to drive interactivity. This is often called reactivity and the most well known instance of this approach are spreadsheet applications like Excel. When you reference a particular cell in the formula of another cell, changing the original cell will automatically trigger an update in all cells that reference. The same concept applies to Parameter objects.

One of the main things to understand about Param in the context of its use in Panel is the distinction between the Parameter value and the Parameter object. The value represents the current value of a Parameter at a particular point in time, while the object holds metadata about the Parameter but also acts as a reference to the Parameter’s value across time. In many cases you can pass a Parameter object and Panel will automatically resolve the current value and reactively update when that Parameter changes. Let’s take a widget as an example:

text = pn.widgets.TextInput()

text.value # 👈 the "value" Parameter of this widget reflects the current value
text.param.value # 👈 can be used as a reference to the live value

We will dive into this more deeply later, for now just remember that parameter objects (whether associated with widgets or not) allow you to pass around a reference to a value that automatically updates if the original value changes.

Display and rendering#

Panel aims to let you work with all your favorite Python libraries and has a system for automatically inferring how to render a particular object, whether that is a pandas DataFrame, matplotlib Figure or any other plotting object. This means that you can easily place any object you want to render into a layout (such as a Row or Column) and Panel will automatically figure out the appropriate Pane type to wrap it. Different Pane types know how to render different objects but also provide ways to update the object or even listen to events such as selection of Vega/Altair charts or Plotly plots.

For this reason, it often makes sense to get a handle on the Pane type. So if you want to wrap your DataFrame into a pane you can call the panel function and it will automatically convert it (this is exactly what a layout does internally when you give it an object to render):

import pandas as pd

df = pd.DataFrame({
  'A': [1, 2, 3, 4],
  'B': [10, 20, 30, 40]
})

df_pane = pn.panel(df)

Tip

To inspect the type of an object simply print it:

>>> print(pn.Row(df))
Row
    [0] DataFrame(DataFrame)

Sometimes an object has multiple possible representations to pick from. In these cases you can explicitly construct the desired Pane type, e.g. here are a few representations of a DataFrame:

pn.pane.DataFrame(df)
A B
0 1 10
1 2 20
2 3 30
3 4 40
pn.pane.HTML(df)
A B
0 1 10
1 2 20
2 3 30
3 4 40
pn.pane.Str(df)
   A   B
0  1  10
1  2  20
2  3  30
3  4  40

Learn More

Learn more about Panes in the Explanation for Components

So far we have only learned how to display data, but to actually add it to your deployed application you also need to mark it as servable. To mark an object as servable adds it to the current template, something we will get into later. You can either mark multiple objects as servable which will add it to the page sequentially, or you can use layouts to arrange objects explicitly.

df_pane.servable()

Note

In the notebook the .servable() method is effectively a no-op. This means you can add it the components you want to add to the rendered app but also see it rendered inline. This makes it possible to build components sequentially in a notebook while simultaneously building an application to be served. If you want to mark something servable but do not want it rendered inline, just put a semicolon (‘;’) after it to tell Jupyter not to render it even if it is the last item in the cell.

Widgets#

To build an interactive application you will typically want to add widget components (such as TextInput, FloatSlider or Checkbox) to your application and then bind them to an interactive function. As an example let’s create a slider:

import panel as pn

x = pn.widgets.IntSlider(name='x', start=0, end=100)

def square(x):
    return f'{x} squared is {x**2}'

pn.Row(x, pn.bind(square, x))

The pn.bind function lets us bind a widget or a Parameter object to a function that returns an item to be displayed. Once bound, the function can be added to a layout or rendered directly using pn.panel and .servable(). In this way you can express reactivity between widgets and output very easily. Even better, if you use a Panel-aware library like hvPlot, you often don’t even need to write and bind a function explicitly, as hvPlot’s .interactive DataFrames already create reactive pipelines by accepting widgets and parameters for most arguments and options.

Reminder

Remember how we talked about the difference between a Parameter value and a Parameter object. In the previous example the widget itself is effectively an alias for the Parameter object, i.e. the binding operation is exactly equivalent to pn.bind(square, x.param.value). This is true for all widgets: the widget object is treated as an alias for the widget’s value Parameter object. So you can generally either pass in the widget (as a convenient shorthand) or the underlying Parameter object.

The binding approach above works, but it is quite heavy handed. Whenever the slider value changes, Panel will re-create a whole new Pane and re-render the output. If we want more fine-grained control we can instead explicitly instantiate a Markdown pane and pass it bound functions and Parameters by reference:

import panel as pn

x = pn.widgets.IntSlider(name='x', start=0, end=100)
background = pn.widgets.ColorPicker(name='Background', value='lightgray')

def square(x):
    return f'{x} squared is {x**2}'

def styles(background):
    return {'background-color': background, 'padding': '0 10px'}

pn.Column(
    x,
    background,
    pn.pane.Markdown(pn.bind(square, x), styles=pn.bind(styles, background))
)

To achieve the same thing using more classic callbacks we have to dig a bit further into Param functionality to learn about watching Parameters. To watch a Parameter means to declare a callback that fires when the Parameter’s value changes. As an example let’s rewrite the example above using a watcher:

import panel as pn

x = pn.widgets.IntSlider(name='x', start=0, end=100)
background = pn.widgets.ColorPicker(name='Background', value='lightgray')

square = pn.pane.Markdown(
    f'{x.value} squared is {x.value**2}',
    styles={'background-color': background.value, 'padding': '0 10px'}
)

def update_square(event):
    square.object = f'{event.new} squared is {event.new**2}'

def update_styles(event):
    square.styles = {'background-color': event.new, 'padding': '0 10px'}

x.param.watch(update_square, 'value')
background.param.watch(update_styles, 'value')

pn.Row(x, background, square)

The first thing you will note is how much more verbose this is, which should make you appreciate the power of expressing reactivity using Parameter binding instead. But if you do need this power, it’s there for you!

Templates#

Once you have started to build an application, you will probably want to make it look nicer, which is where templates come in. Whenever you mark an object as .servable you are inserting it into a template. By default Panel uses a completely blank template, but it is very simple to select another template by setting pn.config.template. Here you will have a few options based on different frameworks, including 'bootstrap', 'material' and 'fast'.

pn.config.template = 'fast'

Note

The pn.config object provides a range of options that will allow you to configure your application. As a shortcut, you may instead provide options for the config object as keywords to the pn.extension call. In other words pn.extension(template='fast') is equivalent to pn.config.template = 'fast', providing a clean way to set multiple config options at once.

Once you have configured a template you can control where to render your components using the target argument of the .servable() method. Most templates have multiple target areas including ‘main’, ‘sidebar’, ‘header’ and ‘modal’. As an example you might want to render your widgets into the sidebar and your plots into the main area:

import numpy as np
import matplotlib.pyplot as plt
import panel as pn

pn.extension(template='fast')

freq = pn.widgets.FloatSlider(
    name='Frequency', start=0, end=10, value=5
).servable(target='sidebar')

ampl = pn.widgets.FloatSlider(
    name='Amplitude', start=0, end=1, value=0.5
).servable(target='sidebar')

def plot(freq, ampl):
    fig = plt.figure()
    ax = fig.add_subplot(111)
    xs = np.linspace(0, 1)
    ys = np.sin(xs*freq)*ampl
    ax.plot(xs, ys)
    return fig

mpl = pn.pane.Matplotlib(
    pn.bind(plot, freq, ampl)
)

pn.Column(
    '# Sine curve', mpl
).servable(target='main')

Next Steps#

While Getting Started, you have installed Panel, built a simple app, and reviewed core concepts - the basic foundations for you to start using Panel for your own work.

While working, you can find solutions to specific problems in the How-to section and you can consult the API Reference or Component Gallery sections for technical specifications, descriptions, or examples.

If you want to gain clarity or deepen your understanding on particular topics, refer to the Explanation section of the docs. For example, the Explanation > APIs subsection covers the benefits and drawbacks of each supported Panel API.

Getting Help#

Check out the HoloViz Community page for all of the options to connect with developers and other users.