Add interactivity with generators#

This guide addresses how to use generators to build interactive components. This is done with the use of pn.bind, which binds a function or method to the value of a widget. Compared to simple reactive functions this allows for more complex interactivity.


import asyncio
import time

import panel as pn

pn.extension()

Let us say we have some action that is triggered by a widget, such as a button, and while we are computing the results we want to provide feedback to the user. Using imperative programming this involves writing callbacks that update the current state of our components. This is complex and really we prefer to write reactive components. This is where generator functions come in.

Important

A generator function is a function that use yield to return results as they are produced during the execution. It is not allowed to return anything, but can use return to break the execution. For an introduction to generator functions check out Real Python | Introduction to generator functions.

In the example below we add a Button to trigger some calculation. Initially the calculation hasn’t yet run, so we check the value provided by the Button indicating whether a calculation has been triggered and while it is False we yield some text and return. However, when the Button is clicked the function is called again with run=True and we kick off some calculation. As this calculation progresses we can yield updates and then once the calculation is successful we yield again with the final result:

run = pn.widgets.Button(name="Press to run calculation", align='center')

def runner(run):
    if not run:
        yield "Calculation did not run yet"
        return
    for i in range(101):
        time.sleep(0.01) # Some calculation
        yield pn.Column(
            f'Running ({i}/100%)', pn.indicators.Progress(value=i)
        )
    yield "Success ✅︎"
pn.Row(run, pn.bind(runner, run))

This provides a powerful mechanism for providing incrememental updates as we load some data, perform some data processing, etc.

This can also be combined with asynchronous processing, e.g. to dynamically stream in new data as it arrives:

import random

async def slideshow():
    index = 0
    while True:
        url = f"https://picsum.photos/800/300?image={index}"

        if pn.state._is_pyodide:
            from pyodide.http import pyfetch
            img, _ = await asyncio.gather(pyfetch(url), asyncio.sleep(1))
            yield pn.pane.JPG(await img.bytes())

        import aiohttp
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                img, _ = await asyncio.gather(resp.read(), asyncio.sleep(1))
                yield pn.pane.JPG(img)
        index = (index + 1) % 10

pn.Row(slideshow)