An example diagram of chained callbacks with inputs and ouputs in Dash plotly

Tutorial: Dash callbacks (schemas & examples)

At its core, Dash Plotly uses a system of “callbacks” to create interactive features – these are Python functions that automatically update parts of your application in response to user inputs.

There are many ways to design Dash callbacks, and in this dash callbacks tutorial, I’ll provide a comprehensive, step-by-step guide with diagrams and code examples. By the end of this tutorial, you’ll have a good understanding of how callbacks work and how to implement interactivity in your own Dash applications.

Let’s start! đź’Ş

A simple callback

To get started, let’s explore a minimal Dash app that features a simple counter and a button to increment its value:

The associated code is the following:

from dash import Dash, html, callback, Input, Output, PreventUpdate

app = Dash(__name__)

# Declare the layout
app.layout = html.Div([
    html.P('Count: 0', id='counter'),
    html.Button('Click', id='btn', n_clicks=0)
])

# Define the callback
@app.callback(
    Output('counter', 'children'), 
    Input('btn', 'n_clicks')
)
def update(n_clicks):
    return f"Count: {n_clicks}"

# Run the app
if __name__ == '__main__':
    app.run_server(debug=True)

The first lines define the app layout. The syntax is highly inspired by HTML elements, but elements are called “components” in Dash. These components are more than just static elements; they are interactive and seamlessly integrate with Python callbacks to enable dynamic updates.

app.layout = html.Div([
    html.P('Count: 0', id='counter'),  # same as <p id="counter">Count: 0</p> in HTML
    html.Button('Click', id='btn', n_clicks=0)  # same as <button id="btn">Click</button> in HTML
])

We explicitly set n_clicks to 0 to initialize the button with a default value. Without this, its value would be None, but the update function is supposed to work with an integer.

The next lines define the callback, which is a core concept in Dash. Dash callbacks are just regular Python functions decorated with the @app.callback decorator (or just @callback). This decorator connects input components (such as button clicks) with output components (like the displayed counter text):

@app.callback(
    # the parameters are explicitly written now, but usually we don't
    Output(component_id='counter', component_property='children'), 
    Input(component_id='btn', component_property='n_clicks'),
)
def update_counter(n_clicks):
    # ...

Inputs and Outputs must have a specified component ID and property to connect. This makes it clear which actions update the app and what parts of the app are changed. For example, the button’s n_clicks property triggers the callback, updating the paragraph’s children property with the new count.

If you are not familiar with decorators, I suggest you take a look at this tutorial: https://realpython.com/primer-on-python-decorators/

Dash callbacks are essential as they enable interactivity. By linking inputs and outputs, they dynamically update the app in response to user actions.

flowchart LR
    btn["Button<br>id='btn2'"] -->|"n_clicks<br>Input"| update_counter(["update<br>callback"])
    update_counter -->|"children<br>Output"| counter["Paragraph<br>id='counter'"]
    style btn fill:#f9f,stroke:#333
    style counter fill:#bbf,stroke:#333
    style update_counter fill:#ff9,stroke:#333

In this example, the counter is incremented each time the button is pressed. The update_counter function encapsulates this logic and returns the result to be updated within the paragraph.


Congratulations, you just learned the most essential feature of Dash 🙂

Multi input callback

Now, let’s take it a step further. What if you want multiple components to control the same output? Dash makes this possible by supporting multiple inputs in a single callback.

Imagine you have two buttons: one adds 1 for each click, and the other subtracts 1 for each click.

Here’s how you can implement it:

from dash import Dash, html, callback, Input, Output, PreventUpdate

app = Dash(__name__)

# Declare the layout
app.layout = html.Div([
    html.P('Count: 0', id='counter'),
    html.Button('Click +', id='btn_add', n_clicks=0), 
    html.Button('Click -', id='btn_sub', n_clicks=0), # two buttons
])

# Define the callback
@app.callback(
    Output('counter', 'children'), 
    Input('btn_add', 'n_clicks'),
    Input('btn_sub', 'n_clicks') # now we have 2 inputs
)
def update_counter(n_clicks1, n_clicks2):
    return f"Count: {n_clicks1-n_clicks2}"

# Run the app
if __name__ == '__main__':
    app.run_server(debug=True)

To do so, we simply added an extra Input in our @app.callback decorator. As a result, we now have 2 possible triggers:

flowchart LR
    btn1["Button<br>id='btn_add'"] -->|"n_clicks<br>Input"| update_counter(["update<br>callback"])
    btn2["Button<br>id='btn_sub'"] -->|"n_clicks<br>Input"| update_counter(["update<br>callback"])
    update_counter -->|"children<br>Output"| counter["Paragraph<br>id='counter'"]
    style btn1 fill:#f9f,stroke:#333
    style btn2 fill:#f9f,stroke:#333
    style counter fill:#bbf,stroke:#333
    style update_counter fill:#ff9,stroke:#333

In some cases, two inputs can be fired at the same time. Dash therefore handles it efficiently by triggering the callback only once. In our example, it means that the counters would update once even if the two buttons are clicked exactly at the same time.


Multiple inputs and outputs callbacks

The real excitement begins when we need to update multiple components simultaneously. For example, we can enhance the counter by displaying the result in red if it’s negative or green if it’s positive.

To achieve this, we update the “style” property of the component alongside its text. This involves adding another Output in our callback and adjusting the function accordingly:

@app.callback(
    Output('counter', 'children'), 
    Output('counter', 'style'),  # here we added property "style"
    Input('btn_add', 'n_clicks'),
    Input('btn_sub', 'n_clicks')
)
def update_counter(n_clicks1, n_clicks2):

    content = f"Count: {n_clicks1-n_clicks2}"

    if n_clicks1-n_clicks2 >= 0:
        style = {'color': 'green'}
    else:
        style = {'color': 'red'}

    return content, style  # as a result, we now return two values

Notice that now the function returns two variables: the content (for children property), and style (for style property).

If inputs can trigger a callback independantly or together, all outputs of a callback are always modified at the exact same time. It means that Dash will always update children and style, whenever the callback is triggered.

flowchart LR
    btn1["Button<br>id='btn_add'"] -->|"n_clicks<br>Input"| update_counter(["update<br>callback"])
    btn2["Button<br>id='btn_sub'"] -->|"n_clicks<br>Input"| update_counter(["update<br>callback"])
    update_counter -->|"children<br>Output"| counter["Paragraph<br>id='counter'"]
    update_counter -->|"style<br>Output"| counter["Paragraph<br>id='counter'"]
    style btn1 fill:#f9f,stroke:#333
    style btn2 fill:#f9f,stroke:#333
    style counter fill:#bbf,stroke:#333
    style update_counter fill:#ff9,stroke:#333

Good to know: it is still possible to not update an output by providing dash.no_output as value for an output.


Multiple callbacks with the same inputs

Dash does not limit the amount of callbacks attached to an input. However, it is theoretically forbidden to have more than one callback per output (= only one callback should be responsible for the update of a component’s property).

I wrote ‘theoretically‘ because it’s possible with the callback parameter allow_duplicate_output. However, you should avoid using it a much as possible as it makes a code more complicated to debug. See more here: https://dash.plotly.com/duplicate-callback-outputs

Let’s add a Progress bar to our counter. We need to update this progress bar every time that the buttons are clicked, hence a new update_pbar callback:

# Declare the layout
app.layout = html.Div([
    html.P('Count: 0', id='counter'),
    html.Progress(id="progress_bar", value=0, max=10), # We add the progress bar component
    html.Br(),
    html.Button('Click +', id='btn_add', n_clicks=0),
    html.Button('Click -', id='btn_sub', n_clicks=0),
])

# (... update counter)

@app.callback(
    Output('progress_bar', 'value'), 
    Input('btn_add', 'n_clicks'),
    Input('btn_sub', 'n_clicks')
)
def update_pbar(n_clicks1, n_clicks2):
    # This is a new callback that only updates the progress bar. 
    return n_clicks1-n_clicks2

As you can see, this callback is simple, only the output is different from the very first callback we implemented. The callback graph might now look like:

flowchart LR
    btn1["Button<br>id='btn_add'"] -->|"n_clicks<br>Input"| update_counter(["update_counter<br>callback"])
    btn2["Button<br>id='btn_sub'"] -->|"n_clicks<br>Input"| update_counter(["update_counter<br>callback"])
    update_counter -->|"children<br>Output"| counter["Paragraph<br>id='counter'"]
    update_counter -->|"style<br>Output"| counter["Paragraph<br>id='counter'"]
    
    btn1["Button<br>id='btn_add'"] -->|"n_clicks<br>Input"| update_pbar(["update_pbar<br>callback"])
    btn2["Button<br>id='btn_sub'"] -->|"n_clicks<br>Input"| update_pbar(["update_pbar<br>callback"])
    update_pbar -->|"value<br>Output"| pbar["Progress<br>id='progress_bar'"]
    
    style btn1 fill:#f9f,stroke:#333
    style btn2 fill:#f9f,stroke:#333
    style counter fill:#bbf,stroke:#333
    style pbar fill:#bbf,stroke:#333
    style update_counter fill:#ff9,stroke:#333
    style update_pbar fill:#ff9,stroke:#333

Note: In this example, we could have updated both the progress bar and the counter in one callback. Whether to group callbacks or keep them separate depends on your app’s needs. Take a look at this article: Dash Callbacks best practices.

Theorically, these two callbacks should run at the same time. In practice, dash will first trigger on callback and then the second, with no specific order. If you absolutely need one callback to be triggered before the other, then you can chain callbacks (as described in the next section).

True parallel processing is often more complex : it requires having to CPUs (or two servers) running the app. It depends on how the app is deployed, how the server handles the request, etc…. Most of the time, the callbacks are just run one after the other.


Chained callbacks: callbacks triggrering other callbacks

As your app grows, the logic for updating outputs may depend on multiple interconnected inputs. This is where chaining callbacks becomes an invaluable tool. By breaking down complex updates into smaller, linked callbacks, you can maintain a clear and scalable workflow.

Let’s now update our little app and show a multiplier slider. It will multiply the click count with the slider value :

# Declare the layout
app.layout = html.Div([
    html.P('Count: 0', id='counter'),
    html.Progress(id="progress_bar", value=0, max=50),
    html.Br(),
    html.Button('Click +', id='btn_add', n_clicks=0),
    html.Button('Click -', id='btn_sub', n_clicks=0),
    html.Br(),
    html.Label('Multiplier:'),
    dcc.Slider(id='multiplier', min=1, max=5, value=1, step=1)
], style={"max-width": "300px"})

@app.callback(
    Output('counter', 'children'), 
    Output('counter', 'style'), 
    Input('btn_add', 'n_clicks'),
    Input('btn_sub', 'n_clicks'),
    Input('multiplier', 'value')
)
def update_counter(n_clicks1, n_clicks2, multiplier):
    count = (n_clicks1 - n_clicks2) * multiplier
    content = f"Count: {count}"

    if count >= 0:
        style = {'color': 'green'}
    else:
        style = {'color': 'red'}

    return content, style

@app.callback(
    Output('progress_bar', 'value'), 
    Input('btn_add', 'n_clicks'),
    Input('btn_sub', 'n_clicks'),
    Input('multiplier', 'value')
)
def update_pbar(n_clicks1, n_clicks2, multiplier):
    return (n_clicks1 - n_clicks2) * multiplier

We now have 3 inputs. And this number will increase as the app gets more complex ; and it will get worse when adding more callbacks that use the count result.

A good solution would be to store this count result into memory using a dcc.Store component. The dcc.Store is a special Dash component that allows you to store data in the browser’s memory – think of it as a variable that persists between callbacks. It’s invisible to users but can hold any JSON-serializable data (numbers, strings, lists, or dictionaries). This component is particularly useful for:

  • Sharing data between callbacks without passing it through visible components
  • Reducing the number of redundant calculations
  • Storing intermediate results that multiple callbacks need to access

Learn more about the dcc.Store component in the official documentation: https://dash.plotly.com/sharing-data-between-callbacks

Here’s how we can implement it:

app.layout = html.Div([
    dcc.Store(id='count_memory', data=0),  # Store component to save the count
    # ...
], style={"max-width": "300px"})

@app.callback(
    Output('count_memory', 'data'),  # Update the stored count
    Input('btn_add', 'n_clicks'),
    Input('btn_sub', 'n_clicks'),
    Input('multiplier', 'value'),
    State('count_memory', 'data')  # Access the current stored count
)
def update_store(n_clicks1, n_clicks2, multiplier, current_count):
    count = (n_clicks1 - n_clicks2) * multiplier
    return count

@app.callback(
    Output('counter', 'children'), 
    Output('counter', 'style'), 
    Input('count_memory', 'data')
)
def update_counter_display(count):
    content = f"Count: {count}"
    style = {'color': 'green'} if count >= 0 else {'color': 'red'}
    return content, style

@app.callback(
    Output('progress_bar', 'value'), 
    Input('count_memory', 'data')
)
def update_pbar(count):
    return count

We now have a callback responsible for the computation (update_store) and two callbacks responsible for display (update_counter_display and update_pbar). This gives the following diagram:

flowchart LR
    btn1["Button<br>id='btn_add'"] -->|"n_clicks<br>Input"| update_store(["update_store<br>callback"])
    btn2["Button<br>id='btn_sub'"] -->|"n_clicks<br>Input"| update_store
    multiplier["Input<br>id='multiplier'"] -->|"value<br>Input"| update_store
    store["Store<br>id='count_memory'"] -.->|"data<br>State"| update_store
    update_store -->|"data<br>Output"| store
    
    store -->|"data<br>Input"| update_counter(["update_counter<br>callback"])
    update_counter -->|"children<br>Output"| counter["Paragraph<br>id='counter'"]
    update_counter -->|"style<br>Output"| counter
    
    store -->|"data<br>Input"| update_pbar(["update_pbar<br>callback"])
    update_pbar -->|"value<br>Output"| pbar["Progress<br>id='progress_bar'"]

    style btn1 fill:#f9f,stroke:#333
    style btn2 fill:#f9f,stroke:#333
    style multiplier fill:#f9f,stroke:#333
    style counter fill:#bbf,stroke:#333
    style pbar fill:#bbf,stroke:#333
    style update_store fill:#ff9,stroke:#333
    style update_counter fill:#ff9,stroke:#333
    style update_pbar fill:#ff9,stroke:#333
    style store fill:#bbf,stroke:#333

Using State instead of Input

You’ll notice in our update_store callback that we use State('count_memory', 'data') instead of Input('count_memory', 'data'). This is because we want to access the current stored count but don’t want changes to it to trigger this callback. If we used Input, we’d create a circular dependency: the store update would trigger the callback, which would update the store, which would trigger the callback again!

Using State allows us to read the stored value only when we need it, while the actual triggers for our callback remain the button clicks and multiplier changes. This pattern of using State to access stored values is common when working with dcc.Store components – it gives you access to persistent data without creating unintended callback chains.

TL;DR. State does not trigger a callback while an Input will. But both can be used to retrieve a component’s property.

Advantages and disadvantages of chaining callbacks

Chained callbacks help keep your code clean and logical. Instead of cramming everything into one big callback, you can break things down into smaller pieces that each do one job well. For example, one callback handles the data processing while another takes care of displaying it. This approach also opens up more interactive possibilities – when one callback’s output feeds into another’s input, you can create complex interactions like filters that update charts, which then update summary statistics.

But there are some pitfalls to be careful about. Debugging can get tricky when you have callback A triggering B, which triggers C and D, which then trigger even more callbacks… well, good luck figuring out where something went wrong!

You should also watch out for circular dependencies – that’s when your callbacks form a loop (A triggers B triggers C triggers A again). Dash will catch this and throw an error, but it’s a common gotcha when you’re building complex apps.

Want to learn more about avoiding circular dependencies? We have a full article here: How to avoid circular dependancies problem in Dash Plotly


Other types of callbacks

While we’ve covered the main callback patterns, Dash offers several specialized types of callbacks for specific use cases:

Background Callbacks

Background callbacks handle time-consuming operations without blocking your app’s responsiveness. They run intensive tasks in separate threads, making them perfect for heavy computations or long-running processes:

@app.callback(
    Output("results", "children"),
    Input("start", "n_clicks"),
    background=True
)
def slow_computation(n_clicks):
    # Your heavy computation here
    time.sleep(5)  # Simulating long task
    return "Done!"

Learn more in our guide: How to Use Background Callbacks in Dash Plotly

Clientside Callbacks

Clientside callbacks execute directly in the browser, eliminating server round-trips for faster performance. They’re ideal for simple UI updates and responsive interactions, but require JavaScript:

app.clientside_callback(
    """
    function(value) {
        return 'You typed: ' + value
    }
    """,
    Output('output-div', 'children'),
    Input('input-box', 'value')
)

Learn more: When to use clientside callbacks for Dash Plotly.

Pattern-Matching Callbacks

Pattern-matching callbacks enable handling dynamic sets of components through ID patterns. They’re particularly useful to attach callbacks to components that are created or removed at runtime:

@app.callback(
    Output({'type': 'dynamic-output', 'index': MATCH}, 'children'),
    Input({'type': 'dynamic-input', 'index': MATCH}, 'value')
)
def update_dynamic_output(value):
    return f"You entered: {value}"

Read the guide: How to use Pattern Matching Callbacks (Dash Plotly).

Conclusion

In this article, we explored the main types of Dash callbacks, from simple callbacks to multi-input, multi-output, and chained callbacks.

To deepen your understanding of Dash callbacks, you can:

  • Read the official documentation on Dash callbacks: it’s dense, but there are much more detailed information than in this article (which was focused on giving simple examples).
  • Play with the toy examples above: you could add more inputs, change the computation, add other types of components… be creative! It’s the best to learn.
  • Go deeper with other types of callbacks: you now have the fundation for understand how callbacks triggered. Learn how to use background callbacks, clientside callbacks and patter-matching callbacks.

I hope you enjoyed this Dash callbacks tutorial!