Creating nested UIs using pn.bind

There might be an easier way to define the problem, but I will illustrate it using my usecase.

The use-case:

  • A python method that takes a data object as input is created. This method encapsulates how to view this data object. It contains a drop-down, based on the value selected the data_object is transformed.
    Example code for illustration:
data_object_1 = {
    'a': 1,
    'b': 2,
    'c': 3
}


def outer_select(data_obj):
    select_1 = pn.widgets.Select(value='a', options=list(data_obj.keys()))

    def handle_outer_select(val):
        new_data_obj = {
            f'{k}{val}': v for k, v in data_object_1.items()
        }
        return pn.Column(f'Output of select_1 is {new_data_obj}')

    return pn.Column(select_1, pn.bind(handle_outer_select, select_1))
    
outer_select(data_object_1)

This simple code transforms the passed data_object to update it’s keys based on the option selected. This works as expected

  • Now I want to create a separate method to be able to view the transformed data object and call it from within the function defined earlier. This separate method contains a dropdown of it’s own and indexes the input data_object based on the option selected. Here’s the code for this:
data_object_1 = {
    'a': 1,
    'b': 2,
    'c': 3
}

def inner_select(data_obj):
    options = list(data_obj.keys())
    select_2 = pn.widgets.Select(value=options[0], options=options)
    
    def handle_inner_select(val):
        return data_obj[val]
    
    return pn.Column(select_2, pn.bind(handle_inner_select, select_2))

def outer_select(data_obj):
    select_1 = pn.widgets.Select(value='a', options=list(data_obj.keys()))

    def handle_outer_select(val):
        new_data_obj = {
            f'{k}{val}': v for k, v in data_object_1.items()
        }
        return pn.Column(f'Output of select_1 is {new_data_obj}', inner_select(new_data_obj))

    return pn.Column(select_1, pn.bind(handle_outer_select, select_1))
    

outer_select(data_object_1)

Once the selection in the outer dropdown is changed, the dropdown created by the inner_select function does not work.

image

Error in console:

/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/panel/reactive.py _process_events L385
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/parameterized.py update L2282
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/parameterized.py _update L2322
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/parameterized.py _batch_call_watchers L2506
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/parameterized.py _execute_watcher L2468
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/panel/param.py _replace_pane L880
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/panel/pane/base.py _update_inner L708
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/panel/pane/base.py _update_from_object L682
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/panel/pane/base.py _recursive_update L623
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/panel/pane/base.py _recursive_update L653
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/parameterized.py update L2282
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/parameterized.py _update L2322
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/parameterized.py _batch_call_watchers L2506
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/parameterized.py _execute_watcher L2468
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/panel/param.py _replace_pane L865
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/panel/param.py eval L824
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/parameterized.py eval_function_with_deps L162
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/depends.py _depends L41
/home/rohit/miniconda3/envs/datafu/lib/python3.10/site-packages/param/reactive.py wrapped L431
/tmp/ipykernel_1323/2894634017.py handle_inner_select L22
	KeyError: 'ab'

I debugged this and found the cause when I monitored the events raised. Here is a log of all the events being raised when the outer dropdown is changed (The event followed by the object id on which the event is triggered is printed):

Event(what='value', name='value', obj=Select(options=['aa', 'ba', 'ca'], value='ba'), cls=Select(options=['aa', 'ba', 'ca'], value='ba'), old='aa', new='ba', type='changed') 140209924585552  // Inner select changed

Event(what='value', name='value', obj=Select(options=['a', 'b', 'c'], value='b'), cls=Select(options=['a', 'b', 'c'], value='b'), old='a', new='b', type='changed') 140209925717152  // Outer select changed
Event(what='value', name='options', obj=Select(options=['ab', 'bb', 'cb'], value='ab'), cls=Select(options=['ab', 'bb', 'cb'], value='ab'), old=['aa', 'ba', 'ca'], new=['ab', 'bb', 'cb'], type='changed') 140209924585552  // Event raised on inner select
Event(what='value', name='value', obj=Select(options=['ab', 'bb', 'cb'], value='ab'), cls=Select(options=['ab', 'bb', 'cb'], value='ab'), old='ba', new='ab', type='changed') 140209924585552  // Event raised on inner select

The inner “Select” instance triggering events remains the same regardless of the value of the outer select dropdown. Hence the data associated with the inner select handler remains the same. Thus the code fails when the handler tries to query a value that doesnt exist in the data object.

The questions

  1. What is the correct pattern to handle this use-case using pn.bind?
  2. How (if at all) can we force the inner method to create individual “Select” instances instead of sharing a single instance.
  3. How would we achieve this using the declarative API?