How to have non-shared session states

How do I write code that allows different session to have different states?
Currently, all my sessions are sychronized / share their states without explicitly coding it that way.

A minimum example:

import panel as pn
pn.extension()

dashboard = pn.Column('# Dashboard', 
                  pn.Tabs(pn.Spacer(), pn.Spacer(), pn.Spacer())
                 )

server = pn.serve(dashboard, start=True, show=True)

If I now open multiple sessions, even across clients (so different IPs), once I click a tab in one session, all other sessions switch the tab as well.

How do I get to a situation where sessions behave independently?

Two approaches, either use .servable() and launch the dashboard with panel serve or wrap the dashboard components in a function like this:

1 Like

The .servable() approach would cause some more trouble / restructuring with my real code.
But wrapping it in a function works just fine.
Thank you!

1 Like

Adding a subtle point to this topic that I learned the hard way. Regardless of servable(), if using parameterized classes as parameters, you should initialize them in the init and not in the parameter declaration if you want them to be session-independent.

import param
import panel as pn

class SomeParameter(param.Parameterized):
    data = param.DataFrame()

class SomeClass1(param.Parameterized):
    # Initializing SomeParameter at class level will make param1 shared across all sessions
    param1 = param.Parameter(SomeParameter())

class SomeClass2(param.Parameterized):
    param1 = param.Parameter()

    def __init__(self, **params):
        # This will make param1 session independent
        param1 = SomeParameter()
        super().__init__(**params)

4 Likes

@jbogaardt can you still reproduce this?

1 Like

Adding a subtle point to this topic that I learned the hard way. Regardless of servable() , if using parameterized classes as parameters, you should initialize them in the init and not in the parameter declaration if you want them to be session-independent.

@jbogaardt Thanks for that insight.

@jbogaardt can you still reproduce this?

@maximlt : I can replicate it with the following code:

import panel as pn
import param
from panel.viewable import ServableMixin


class TestViewerApp(param.Parameterized):

    view: pn.template.BaseTemplate
    # Parameters
    input_widget = pn.widgets.TextInput(
        name="Input",
        placeholder="Enter Some text ...",
    )
    output_widget = pn.widgets.StaticText(name="Output", value="")

    def __init__(
        self,
        **params,
    ):
        super().__init__(**params)
        self._init_view()

    def _init_view(self) -> None:
        self.view = pn.template.BootstrapTemplate()
        self.view.main.append(self.input_widget)
        self.view.main.append(self.output_widget)

    @param.depends("input_widget.value", watch=True)
    def _update_output(self) -> None:
        inpt = self.input_widget.value
        if inpt is not None:
            self.output_widget.value = f"Input: {inpt!r}"


def session_test_app() -> ServableMixin:
    """A simple test app to check if the platform client is running."""
    return TestViewerApp().view


print(f"test_panel.py {__name__=!r}")
if __name__ == "__main__":
    # Run with `python test_panel.py`
    pn.serve(
        {"session_test": session_test_app},
    )
else:
    # Run with `panel serve test_panel.py`
    session_test_app().servable()

Running this with panel serve test_panel.py doesn’t result in shared state.
Running this with python test_panel.py results in application with shared state (see video):

Btw, initializing the widgets in __init__ as suggested by @jbogaardt does work. For example replacing TestViewerApp with:

class TestViewerApp(param.Parameterized):

    view: pn.template.BaseTemplate
    # Parameters
    input_widget: pn.widgets.TextInput
    output_widget: pn.widgets.StaticText

    def __init__(
        self,
        **params,
    ):
        self.input_widget = pn.widgets.TextInput(
            name="Input",
            placeholder="Enter Some text ...",
        )
        self.output_widget = pn.widgets.StaticText(name="Output", value="")
        super().__init__(**params)
        self._init_view()

    def _init_view(self) -> None:
        self.view = pn.template.BootstrapTemplate()
        self.view.main.append(self.input_widget)
        self.view.main.append(self.output_widget)

    @param.depends("input_widget.value", watch=True)
    def _update_output(self) -> None:
        inpt = self.input_widget.value
        if inpt is not None:
            self.output_widget.value = f"Input: {inpt!r}"

Results in non-shared sessions

Ok, this does not work for all use-cases. The following:

class TestViewerApp(param.Parameterized):

    view: pn.template.BaseTemplate
    # Parameters
    input_widget: pn.widgets.TextInput
    intermediary: str
    output_widget: pn.widgets.StaticText

    def __init__(
        self,
        **params,
    ):
        self.input_widget = pn.widgets.TextInput(
            name="Input",
            placeholder="Enter Some text ...",
        )
        self.output_widget = pn.widgets.StaticText(name="Output", value="")
        self.intermediary = param.String(default="")
        super().__init__(**params)
        self._init_view()

    def _init_view(self) -> None:
        self.view = pn.template.BootstrapTemplate()
        self.view.main.append(self.input_widget)
        self.view.main.append(self.output_widget)

    @param.depends("intermediary", watch=True)
    def _update_output(self) -> None:
        inpt = self.input_widget.value
        if inpt is not None:
            self.output_widget.value = f"Input: {inpt!r}"

Fails with

Traceback (most recent call last):
  File "test_panel.py", line 6, in <module>
    class TestViewerApp(param.Parameterized):
  File "./env/lib/python3.11/site-packages/param/parameterized.py", line 3313, in __init__
    deps, dynamic_deps = _params_depended_on(minfo, dynamic=False)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "./env/lib/python3.11/site-packages/param/parameterized.py", line 681, in _params_depended_on
    ddeps, ddynamic_deps = (minfo.cls if minfo.inst is None else minfo.inst).param._spec_to_obj(d, dynamic, intermediate)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "./env/lib/python3.11/site-packages/param/parameterized.py", line 2860, in _spec_to_obj
    raise AttributeError(f"Attribute {attr!r} could not be resolved on {src}.")
AttributeError: Attribute 'intermediary' could not be resolved on <class '__main__.TestViewerApp'>.

EDIT:

Filed an issue at Serving a panel application using `pn.serve` in a Python script results in shared state · Issue #6489 · holoviz/panel · GitHub

The resulting answer is:

  • Make sure to initialize param Parameters at the class level (To avoid the AttributeError)
  • Make sure to initialize the panel widgets in the __init__ to avoid any shared state.

Hi @peterroelants

The problem in your previous post is that you depend on "intermediary". And that is not a parameter on the class. You define self.intermediary = param.String(default="") in __init__ but that is not an expected/ a supported way of using param.

You should refactor your app taking into account some principles

  • Define param Parameters on your Parameterized class not in the __init__ method.
  • Define your state in your parameters. Use specific widgets (.from_param) to get widgets that can display and change the parameters.
  • Keep your widgets, templates etc. private with _ unless you specifically want to expose them to other users.

The below should work

import panel as pn
import param
from panel.viewable import ServableMixin


class TestViewerApp(pn.viewable.Viewer):

    value_input: str = param.String(label="Input")
    value_output: str = param.String(label="Output")
    
    intermediary: str = param.String()
    
    def __init__(
        self,
        **params,
    ):
        super().__init__(**params)
        
        self._input_widget = pn.widgets.TextInput.from_param(self.param.value_input,
            placeholder="Enter Some text ...",
        )
        self._output_widget = pn.widgets.StaticText.from_param(self.param.value_output)
        self._layout = pn.Column(self._input_widget, self._output_widget)
        
        

    def __panel__(self):
        return self._layout

    def create_template(self):
        return pn.template.BootstrapTemplate(main=[self._layout])       

    @pn.depends("value_input", watch=True, on_init=True)
    def _update_output(self) -> None:
        inpt = self.value_input
        if not inpt:
            self.value_output="Enter Some text ..."
        else:
            self.value_output = f"Input: {inpt!r}"

def session_test_app() -> ServableMixin:
    """A simple test app to check if the platform client is running."""
    return TestViewerApp().create_template()


print(f"test_panel.py {__name__=!r}")
if __name__ == "__main__":
    # Run with `python test_panel.py`
    pn.serve(
        {"session_test": session_test_app},
    )
else:
    # Run with `panel serve test_panel.py`
    session_test_app().servable()
1 Like

Thank @Marc Following these rules seems to work:

  • Initialize param objects at the class level
  • Initialize panel widgets at the object level (__init__). The suggestion to use .from_param here is great.
2 Likes