How to elegantly handle a very large number of Input/State in callbacks?

My dashboard has a bunch of filters, I was looking for a way to handle them neatly in my code. It looks really untidy to have 23 inputs to a function.
Is there a way to have an array of inputs?

@app.callback(
    Output("div", "children"),
        [Input("plot-button", "n_clicks")],
        [State('input1', 'on'),
         State('input2','value'),
         State('input3','value'),
         State('input4','start_date'),
         State('input5','end_date'),
         State('input6','values'),
         State('input7','value'),
         State('input8','value'),
         State('input9','value'),
         State('input10','options'),
         State('input11','value'),
         State('input12','value'),
         State('input13','value'),
         State('input14','value'),
         State('input15','value'),
         State('input16','value'),
         State('input17','value'),
         State('input18','value'),
         State('input19','value'),
         State('input20','value'),
         State('input21','value'),
         State('input22','value'),])
def generate_graph(nclicks, input1, input2, input3, input4, input5, input6, input7, input8, input9, input10, input11, input12, input13, input14, input15, input16, input17, input18, input19, input20, input21, input22):
    //some code
    return

Yes, just do:

def generate_graph(*args):
    ...

*args will be the array of input and state values.

2 Likes

An extension to Philippe’s approach is to have the list of input and states external to the function then form a kwarg dictionary in the function itself so you can refer to inputs via their name rather than position in the args list.

A nice advantage of this is that it these input/state lists can be reused for other callbacks with the same input but different output.

inputs = [Input("plot-button", "n_clicks")]

states = [[State('input1', 'on'),
         State('input2','value'),
         State('input3','value'),
         State('input4','start_date'),
        .........
         State('input19','value'),
         State('input20','value'),
         State('input21','value'),
         State('input22','value'),]

@app.callback(
Output("div", "children"),
inputs,
states)
def generate_graph(*args):

    input_names = [item.component_id for item in inputs + states]

    kwargs_dict = dict(zip(input_names, args))

    //some code

   ....kwargs_dict['input10']....

   //some code

    return
1 Like

If you want to avoid having this boilerplate in every callback function then you can wrap your callback functions in a handler that does it for you!

PS. I hope plot.ly does this for us in the future, hint hint @chriddyp

def create_handler(callback_fn, inputs):

    input_names = [item.component_id for item in inputs]

    def handler(*args):
        kwargs_dict = dict(zip(input_names, args))
        return callback_fn(**kwargs_dict)

    return handler

inputs = [Input("plot-button", "n_clicks")]

states = [[State('input1', 'on'),
         State('input2','value'),
        .........
         State('input22','value'),]

def generate_graph(**kwargs):

    //some code
   ....kwargs['input10']....
   //some code
    return

# Register the callback with the wrapped function
app.callback(
    Output(component_id='cluster-table', component_property='children'),
    inputs,
    states,
    )(create_handler(generate_graph, inputs + states))
6 Likes

I like this approach, maybe we could have an option on the callback to have named argument instead or positional arguments.

Thanks @sjtrny, this looks neat.

Just to tidy this up into a proper decorator

def dash_kwarg(inputs):

    def accept_func(func):

        @wraps(func)
        def wrapper(*args):
            input_names = [item.component_id for item in inputs]
            kwargs_dict = dict(zip(input_names, args))
            return func(**kwargs_dict)

        return wrapper

    return accept_func

inputs = [Input("plot-button", "n_clicks")]

states = [[State('input1', 'on'),
         State('input2','value'),
        .........
         State('input22','value'),]

@app.callback(
    Output(component_id='cluster-table', component_property='children'),
    inputs,
    states,
)
@dash_kwarg(inputs + states)
def generate_graph(**kwargs):

    //some code
   ....kwargs['input10']....
   //some code
    return
1 Like

If you add this to where your app is instantiated, I believe the following would work, too? That way, you can use it very much like a drop-in replacement for where you had @app.callback previously.

"""app.py: The Plotly Dash application instance."""

from functools import wraps
from typing import Any, Callable, List

import dash
from dash.dependencies import Input, Output, State

app = dash.Dash(__name__)


def handler(outputs: List[Output], inputs: List[Input], states: List[State]):
    def accept_func(func: Callable[[Any], Any]):
        @app.callback(outputs, inputs, states)
        @wraps(func)
        def wrapper(*args: Any):
            input_names = [
                d.component_id + "--" + d.component_property for d in inputs + states
            ]
            kwargs_dict = dict(zip(input_names, args))
            return func(**kwargs_dict)

    return accept_func

I also changed the naming a bit, as you could be listening to multiple properties of the same object in your inputs/states. So now it’s “[id]–[property]”

2 Likes

Good thinking! That’s a nice shortcut.

That is really a nice solution. One question though. How can the typing in the handler be specified that a single output/input/state, multiple output/input/state or a list of them is accepted?

Now if I do:

@handler(
    Output(component_id='', component_property=''),
    Input(component_id='', component_property='')
)

Pycharm marks the Output and Input statements with the comment that it is expecting lists. The beauty of the latest version of Dash in my opinion is that you don’t have to enclose the inputs in lists anymore.

I’ll remove the typing info for now anyways.

Edit: Btw, has this been integrated in Dash already?

Edit 2: After some experimentation, I see that inputs and states have to be lists, otherwise the list comprehension in input_names doesn’t work

Hi @Tobs and welcome to the Dash community!

The solution for this use-case is Pattern Matching Callbacks which was implemented after the last post in this topic was active. This is an old thread and will be closed, so if you have more questions after reading through the pattern matching callback documentation, then please feel free to ask it in a new topic.