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:
- Do you prefer the new features or the current way? why?
- Is there anything is the new features you found confusing?
- 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 fordash.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]
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
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"}}
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}
}
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()
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"
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