Is the Order of Dash Callback Context Inputs & States guaranteed?

I notice that in a callback there are 2 contexts:

dash.callback_context.inputs
dash.callback_context.states

And they have the same order as the order I receive my arguments in the callback. And in Python 3.7+ dictionaries order is guaranteed to be the same as creation / insertion order.

So can I rely on the order of these callback_contexts? It will allow me to write some very clean code if so.

This isn’t officially part of the API until we use something official and intentional like OrderedDict. I don’t imagine that this will change, but it could happen.

Here’s an issue to track progress on this. We’d also probably accept a PR on this too.

I understand for Python 2.7 and pre-(CPython 3.6 / Python 3.7) you can’t guarantee the order due to the the lack of Dictionaries have order. But as long as I am in CPython 3.6+/ Python 3.7+ I can assume same order as order of arguments?

Let me give you some sample code that I generated to demonstrate how attractive this ends up being:

import dash
import dash_html_components as html
from random import randint

NUMBER_OF_BUTTONS = 30

app = dash.Dash(__name__)

buttons = []
for button_num in range(NUMBER_OF_BUTTONS):
    buttons.append(
        html.Div(
            html.Span(
                [html.Button('On / Off',
                             id=f'on-off-button-{button_num}'),
                 html.Span(' On',
                           id=f'on-text-{button_num}',
                           style={'display': 'none',
                                  'color': f'#{randint(0, 255):02X}'
                                           f'{randint(0, 255):02X}'
                                           f'{randint(0, 255):02X}'})]
            )
        )
    )

app.layout = html.Div(buttons)

# Single callback
on_off_outputs = []
on_off_inputs = []
on_off_states = []
for button_num in range(NUMBER_OF_BUTTONS):
    on_off_outputs.append(dash.dependencies.Output(f'on-text-{button_num}', 'style'))
    on_off_inputs.append(dash.dependencies.Input(f'on-off-button-{button_num}', 'n_clicks'))
    on_off_states.append(dash.dependencies.State(f'on-text-{button_num}', 'style'))


@app.callback(output=on_off_outputs, inputs=on_off_inputs, state=on_off_states)
def on_off_change(*args):
    if all(v is None for v in dash.callback_context.inputs.values()):
        raise dash.exceptions.PreventUpdate

    triggered_name = dash.callback_context.triggered[0]['prop_id']
    triggered_value = dash.callback_context.triggered[0]['value']
    return_values = []
    for (input_name, input_value), state_value in zip(dash.callback_context.inputs.items(),
                                                      dash.callback_context.states.values()):
        if input_name == triggered_name:
            if triggered_value % 2 == 1:
                state_value['display'] = ''
                return_values.append(state_value)
            else:
                state_value['display'] = 'None'
                return_values.append(state_value)
        else:
            return_values.append(state_value)

    return return_values


if __name__ == '__main__':
    app.run_server()

In this case the number of buttons is a variable but we have one very simple callback that we didn’t need to do any name look-ups on, just iterate of input and state. I suspect this works so well that unless warn people not to do it if you ever break it in the future you will get bug reports :slight_smile: