Dash 2.x Feature Preview: Simplified callbacks

Hey Dash Community,

I’m excited about some new features we’re working on to make callbacks easier, more concise and use less boilerplate. I have a pull request in the works and would love to get some community feedback while it’s under review. Helpful questions to think about:

  1. Do you prefer the new features or the current way? why?
  2. Is there anything is the new features you found confusing?
  3. Anything that can be improved?

Improvements

  • Simplifies the syntax for determining which input triggered a callback.
  • Makes code more concise by adding ctx as an alias for dash.callback_context
  • Adds the convenience of using dot notation to access the ctx dictionary
  • Simplifies the callback function signature for callbacks with many Inputs.
  • Makes it easier to get the dictionary ids when using pattern matching callbacks.

Background

Currently, when you have multiple Inputs in a callback, you might do something like this to determine which input triggered the callback:

@callback(
    Output("graph", "figure"), Input("btn-1", "n_clicks"), Input("btn-2", "n_clicks")
)
def display(btn1, btn2):
    ctx = dash.callback_context
    button_id = ctx.triggered[0]['prop_id'].split('.')[0]

    if button_id == "btn-1":
         return update_graph1()
    if button_id == "btn-2":
         return update_graph2()

One of the goals is to eliminate boilerplate such as:

ctx = dash.callback_context
button_id = ctx.triggered[0]['prop_id'].split('.')[0]

:point_right: Introducing: dash.ctx

ctx is an new concise name for dash.callback_context`. It’s convenient to include with the import statements like this:

from dash import Dash, html, Input, Output, ctx

Now you can eliminate this line of code

ctx = dash.callback_context

:point_right: Introducing: ctx.triggered_ids

You can use the new ctx.triggered_ids dictionary to see which input triggered the callback:

Tada: No boilerplate!

@callback(
    Output("graph", "figure"), Input("btn-1", "n_clicks"), Input("btn-2", "n_clicks")
)
def display(btn1, btn2):
   if  "btn-1.n_clicks" in ctx.triggered_ids:
       return update_graph1()
   if  "btn-2.n_clicks" in ctx.triggered_ids:
      return update_graph2()

ctx.triggered_ids is a dictionary of the component ids and props that triggered the callback.
When using pattern matching callbacks, it also has the dictionary form of the ids rather than just the stringified version with no white spaces that’s available currently.

ctx.triggered_ids:

  • keys (str) : the “prop_id”, composed of the component id and the prop name
  • values (str or dict): component id.

For example, with regular callback, the ctx.triggered_ids dict would look something like:

{"btn-1.n_clicks": "btn-1"}

With pattern matching ids:

{'{"index":0,"type":"filter-dropdown"}.value': {"index":0,"type":"filter-dropdown"}}


:point_right: Introducing: ctx.args_grouping

Sure, the above syntax using the new ctx.triggered_ids is an improvement, but wouldn’t in be great if you could use a variable name instead of the component id and prop string? Maybe something like:

if variable_name.triggered:
     do_something()

Well, now you can! By using flexible callback signatures and the new ctx.args_grouping, your callback can look like this:

@app.callback(
    Output("container", "children"),
    inputs=dict(btn1=Input("btn-1", "n_clicks"), btn2=Input("btn-2", "n_clicks")),
)
def display(btn1, btn2):
    c = ctx.args_grouping
    if c.btn1.triggered:
        return f"Button 1 clicked {btn1} times"
    elif c.btn2.triggered:
        return f"Button 2 clicked {btn2} times"
    else:
        return "No clicks yet"

ctx.args_grouping is a dict of the inputs used with flexible callback signatures. The keys are the variable names and the values are dictionaries containing:

  • “id”: (string or dict) the component id. If it’s a pattern matching id, it will be a dict.
  • “id_str”: (str) for pattern matching ids, it’s the strigified dict id with no white spaces.
  • “property”: (str) The component property used in the callback
  • “value”: the value of the component property at the time the callback was fired
  • “triggered”: (bool)Whether this input triggered the callback

So, in the above example, if you click the Button 1, then ctx.args_grouping would contain:

{
    'btn1': {'id': 'btn-1', 'property': 'n_clicks', 'triggered': True, 'value': 1},
    'btn2': {'id': 'btn-2',   'property': 'n_clicks', 'triggered': False,  'value': None}
}

:point_right: New: Access the ctx.args_grouping dict with a dot notation:

To get the value of the callback Input, wouldn’t you rather type c.btn1.value than c["btn1"]["value"]?

c = ctx.args_grouping

# get the prop values like this:
c["btn1"]["value"]

# Or you can use the dot notation:
c.btn1.value

# See if it triggered the callback like this:
if c.btn1.triggered:
   do_something()


:cake: Simplified callback function signature

Here’s my favorite thing about the new ctx.args_grouping: You don’t need to repeat all the variable names in the callback function. This is super convenient when you have a large callback with many inputs.

See issue #1810 Feature Request: Support Dash callback closer to function signature
and the related discussion on the forum: Proposal: New callback syntax closer to function signature

I think this can also make learning callbacks easier for people new to Dash. There is often confusion around how variables are named in the function, as discussed in this post Newbie finds naming snobbish

In the example below note that:

  • all_inputs is the only arg in the callback function:
  • The variable names are beside each input under the callback decorator

@app.callback(
    Output("container", "children"),
    inputs={
        "all_inputs": {
            "btn1": Input("btn-1", "n_clicks"),
            "btn2": Input("btn-2", "n_clicks"),
            "btn3": Input("btn-3", "n_clicks"),
            "dropdown1": Input("dropdown-1", "value"),
            "dropdown2": Input("dropdown-2", "value"),
            "store": State("store", "data"),
        }
    },
)
def display(all_inputs):
    c = ctx.args_grouping.all_inputs  

    if c.btn1.triggered:
        return f"Button 1 clicked {c.btn1.value} times"
    elif c.btn2.triggered:
        return f"Button 2 clicked {c.btn2.value} times"
   #  ... etc
    else:
        "no update"




:cake: Easier to get the dict id when using pattern matching callbacks.

When using pattern matching callbacks, it’s often necessary to get the component’s dictionary id in a callback. However, the current dash.callback_context.triggered only has the "prop_id" which is a stringified dictionary with no white space.
To turn it back into a real Python dictionary, it’s necessary to parse the string and use json loads.

Here is an example of the current dash.callback_context.triggered.

[
  {
    'prop_id': '{"index":0,"type":"filter-dropdown"}.value',
    'value': 'NYC'
  }
]

Skip the string parse and json loads! Instead, use either of the new ctx.triggered_ids or ctx.args_grouping to get the index of the button that triggered the callback.

Example 1: ctx.triggered_ids

import numpy as np
import plotly.express as px
from dash import Dash, Input, Output, ctx, dcc, html, ALL

N=5

app = Dash(__name__)


def make_figure(n):
    x = np.linspace(0, 8, 81)
    y = x ** int(n)
    return px.scatter(x=x, y=y)


app.layout = html.Div(
    [
        html.Div(
            [html.Button(f"x^{i}", id={"index":i}) for i in range(N)]
        ),
        dcc.Graph(id="graph"),
    ]
)

@app.callback(
    Output("graph", "figure"), Input({"index":ALL}, "n_clicks")
)
def update_graph(btns):
    if ctx.triggered:
        
        # This gets the key, which is the stringified dict id: "  
        prop_id=ctx.triggered_ids.first()
        
        # This gets the value which is the component's dictionary `id`:
        dict_id= ctx.triggered_ids[prop_id]
        
        # This gets the index number of the button the triggered the callback.  
        n= dict_id.index
        
        return make_figure(n)
    return {}


if __name__ == "__main__":
    app.run_server(debug=True)

More info on .first()

.first() is a handy method that’s available with the ctx.triggered_ids dictionary.
It acts like .get() but if it has no arguments it returns the first key.


Example 2: ctx.args_grouping

Here is the same app, but instead it uses the new ctx.args_grouping to get the index of the button that triggered the callback:

import numpy as np
import plotly.express as px
from dash import Dash, Input, Output, ctx, dcc, html, ALL

N=5

app = Dash(__name__)


def make_figure(n):
    x = np.linspace(0, 8, 81)
    y = x ** int(n)
    return px.scatter(x=x, y=y)


app.layout = html.Div(
    [
        html.Div(
            [html.Button(f"x^{i}", id={"index":i}) for i in range(N)]
        ),
        dcc.Graph(id="graph"),
    ]
)

@app.callback(
    Output("graph", "figure"), dict(btns=Input({"index":ALL}, "n_clicks"))
)
def update_graph(btns):
    # Since we are using {"index":ALL}, then
    # ctx.args_grouping.btns is a list of dicts - one dict for each button.
    for btn in ctx.args_grouping.btns:
        if btn.triggered:
            n = btn.id.index
            return make_figure(n)
    return {}


if __name__ == "__main__":
    app.run_server(debug=True)



I’m looking forward to feedback on any or all of the new features proposed!

You can find the pull request here: Improved `callback_context` by AnnMarieW · Pull Request #1952 · plotly/dash · GitHub

5 Likes

Awesome PR @AnnMarieW :tada:

I find myself using dash.callback_context in most of my dash projects and each time I had to lookup the docs to copy the boilerplate.
This surely seems to be more convenient, especially having the support for dot notation along with input variable name.

Thanks !

1 Like

Thanks @atharvakatre :blush:

Haha - I have to look up that callback_context code every time too!

In general, I think it looks really nice! Two things pop into my mind though,

  • The ctx name is very concise, but the args_grouping isn’t. It’s rather long and non-intuitive. Could we come up with a better interface for that part?
  • I am missing a convenience function to just get “what triggered the callback” (that’s usually what you want; maybe I missed it?). One of your examples do this manually,
    for btn in ctx.args_grouping.btns:
        if btn.triggered:
          n = btn.id.index
          return make_figure(n)

I think a more convenient syntax would be something like,

    btn = ctx.trigger
    n = btn.id.index
    return make_figure(n)

I implemented a similar approach (but much less elaborate) in dash-extensions where you just get the component that triggered the callback,

When your PR is merged, I guess this code can be dropped :wink:

1 Like

These changes look awesome!

I have two slightly pedantic comments about naming and discoverability

  • from dash import ctx is concise for sure, but if someone new to dash comes across that in code I think it’s a bit opaque, “ctx” doesn’t mean anything to the uninitiated. Typing out dash.callback_context in callbacks is a minor inconvenience for sure, but I think it helps readability generally. I sense I’m in the minority here though.
  • I think ctx.triggered_ids is misleading as a name. I would expect to be able to do if "btn1" in ctx.triggered_ids: rather than if "btn1.n_clicks" in ctx.triggered_ids:. id to me suggests the id of the component rather than the id-prop pair.
1 Like

Hey @Emil and @tcbegley , Thanks for the great feedback I’ll do a combined response since your comments are related

from dash import ctx is concise for sure, but if someone new to dash comes across that in code I think it’s a bit opaque, “ctx” doesn’t mean anything to the uninitiated. Typing out dash.callback_context in callbacks is a minor inconvenience for sure, but I think it helps readability generally.

You make a great point Tom. True, ctx by itself in the import statement is meaningless, but I guess you could say the same thing about dcc. When you look at where it’s used though, I think ctx.triggered is pretty readable. (I’ll admit that when I first started, dash.callback_context didn’t really mean that much to me either.) And of course, callback_context will still be available so anyone who prefers the more descriptive name can still use it.

I think ctx.triggered_ids is misleading as a name. I would expect to be able to do if "btn1" in ctx.triggered_ids: rather than if "btn1.n_clicks" in ctx.triggered_ids:. id to me suggests the id of the component rather than the id-prop pair.

I agree! What do you think of changing the name to ctx.triggered_prop_ids, then adding ctx.triggered_id for just the component id that triggered the callback (as Emil suggested)

I am missing a convenience function to just get “what triggered the callback” (that’s usually what you want; maybe I missed it?).

You’re right Emil, I neglected to include a function that just returns the component id that triggered the callback. My objective was to have one function that covered all the cases – including when more than one Input triggers a callback and when multiple props of the same component are used in a callback. The ctx.triggered_ids dict handles all these cases. (Going forward, let’s call it ctx.triggered_prop_ids)

So now if we have both ctx.triggered_prop_ids and ctx.triggered_id you could use either:

if "btn-1" == ctx.triggered_id
   do_something()

or

if "table-1.selected_rows" in ctx.triggered_prop_ids
    do_something()
if "table-1.selected_columns" in ctx.triggered_prop_ids
    do_something_else()


And now, the pattern matching example above can be written as Emil suggested:

btn = ctx.triggered_id
n = btn.id.index
return make_figure(n)

The ctx name is very concise, but the args_grouping isn’t. It’s rather long and non-intuitive. Could we come up with a better interface for that part?

Emil, could you say more about this? I agree the args_grouping is rather long in the pattern matching callbacks example. The new ctx.triggered_id is a big improvement. Do you think args_grouping is OK otherwise? If not, do you have suggestions for improvement?

1 Like

Nice, yeah I think that’s a good solution.

1 Like

I also like ctx.triggered_id :slight_smile:

1 Like