pythonpanel-pyviz

Panel dashboard: links in multipage with loop not correct


I am trying to create a multipage dashboard with panel.

My question is based on this SO answer:

import panel as pn
from panel.template import FastListTemplate

pn.extension()


class Page1:
    def __init__(self):
        self.content = pn.Column("# Page 1", "This is the content of page 1.")

    def view(self):
        return self.content


class Page2:
    def __init__(self):
        self.content = pn.Column("# Page 2", "This is the content of page 2.")

    def view(self):
        return self.content

def show_page(page_instance):
    main_area.clear()
    main_area.append(page_instance.view())


pages = {
    "Page 1": Page1(),
    "Page 2": Page2()
}

page_buttons = {}
for page in pages:
    page_buttons[page] = pn.widgets.Button(name=page, button_type="primary")
    # page_buttons[page].on_click(lambda event: show_page(pages[page]))

page1_button, page2_button = page_buttons.values()

page1_button.on_click(lambda event: show_page(pages["Page 1"]))
page2_button.on_click(lambda event: show_page(pages["Page 2"]))

sidebar = pn.Column(*page_buttons.values())

main_area = pn.Column(pages["Page 1"].view())

template = FastListTemplate(
    title="Multi-Page App",
    sidebar=[sidebar],
    main=[main_area],
)

template.servable()

If I am uncommenting this line in the for loop

page_buttons[page].on_click(lambda event: show_page(pages[page]))

and remove (or comment as below) the two on_click statements

# page1_button.on_click(lambda event: show_page(pages["Page 1"]))
# page2_button.on_click(lambda event: show_page(pages["Page 2"]))

both links on the sidebar point to page 2.

Can somebody explain to me why this is the case and how I can fix this issue?

Note: Of course, for two pages, a for loop is not needed, however in my case, my app will include a few more pages, and I would like to make the code more robust (i.e. to avoid forgetting to add a page or a click event).

Thank you!

Note: page1_button, page2_button = page_buttons.values() is currently only used because my for loop does not work as intended right now.


Solution

  • You are running into one of the classical footguns in Python: late binding aka "capture by reference".

    By the time your lambda callback is evaluated (on click), the page variable points at page 2 (loop has ended), and both lambdas capture the variable, not its value at time of call.

    The operative line should become:

    page_buttons[page].on_click(lambda event, saved=page: show_page(pages[saved]))
    

    This forces an early binding by storing the current value as a default paramater, while still in the loop context.

    The canonical answer goes into more detail. Creating functions (or lambdas) in a loop (or comprehension).