As a project grows, there is a need to organize the repository for clarity and ease of maintenance.
Here’s a strategy for breaking out a Dash app into manageable pieces. The following structure groups logical parts of the app into different directories, allowing clean, maintainable and scalable architecture for (almost) any Dash project. This layout covers a multi-tab dashboard app with callback definitions in separate files. The important thing here is to import all parts correctly to prevent circular imports and to expose callbacks to the Dash app instance.
The overall structure scales nicely: tabs can be replaced with URLs, data manipulation modules can be added, layout and callbacks can be further split, etc.
In a nutshell, the project structure looks like this.
project/ |--- dashboard/ | |--- assets/ | |--- favicon.ico | |___ style.css | |--- layout/ | |--- callbacks/ | |--- callbacks1.py | |___ callbacks2.py | |--- header.py | |--- tab_1.py | |___ tab_2.py | |--- content.py | |___ index.py |--- app.py |--- Procfile |___requirements.txt
The Dash app is organized into a dir called project with a module app.py, which is the entry point to the app. The actual Dash instance (app) is found in the dashboard/index.py. This separation is required to avoid circular imports (more on that later).
The project directory contains:
- dashboard/ dir (with app instance, layout and assets);
- Procfile for running in Gunicorn;
- requirements.txt with all dependencies;
- app.py the entry point to the app that imports everything else and initiates server (for running in Gunicorn).
# app.py
from dashboard.content import app
server = app.server
if __name__ == "__main__":
app.run_server()
Let’s have a look at the dashboard/ directory. Here we have all the files for application layout and all callbacks organized in separate directories.
- assets/ with custom stylesheets, favicon, logos and other static elements;
- layout/ with app components including header, tabs, callbacks, etc;
- content.py which contains callbacks imports so they can be registered with Dash app instance;
- index.py with Dash app definition.
# index.py
from dash import Dash
import dash_bootstrap_components as dbc
external_stylesheets = [dbc.themes.BOOTSTRAP, "./assets/style.css"]
app = Dash(
__name__,
suppress_callback_exceptions=True,
external_stylesheets=external_stylesheets,
)
app_title = "Dash project structure template"
app.title = app_title
Though callbacks are defined separately, each of them needs to be imported in content.py to be registered with the Dash instance.
# content.py
from dashboard.index import app
from dashboard.layout.callbacks import callbacks1
from dashboard.layout.callbacks import callbacks2
from dashboard.layout.header import header
import dash_html_components as html
import dash_core_components as dcc
tabs = dcc.Tabs(
id="app-tabs",
value="tab-1",
children=[
dcc.Tab(label="Tab1", value="tab-1"),
dcc.Tab(label="Tab2", value="tab-2"),
],
)
tabs_content = html.Div(id="tabs-example-content", className="main-panel")
app.layout = html.Div(
[
header,
tabs,
tabs_content,
]
)
And finally layout/ directory with, as mentioned above, all components for the app and callbacks.
- callbacks/;
- header.py;
- tab_1;
- tab_2.
How to avoid circular imports.
We need the app instance for each callback module, and we need callbacks to be registered with the app. If we put the app and layout in one file, say content.py, this module will import each callback module. But if each callback module tried to import from the content.py, this would create a circular dependency, and the import chain would fail.
This is why we need to separate app instance (index.py) and app layout.
For each callback module, we import the Dash instance from the index.py. And the content.py module imports each callback module, thus avoiding circularity.
To put it simply: first, create app instance (index.py), secondly – create layouts and callbacks (layout/), after that assign layout to the app (content.py), and finally run the app (in app.py).
# callbacks2.py
from dashboard.index import app
from dashboard.layout.tab_1 import tab_1
from dashboard.layout.tab_2 import tab_2
from dash.dependencies import Input, Output
@app.callback(Output("tabs-example-content", "children"), Input("app-tabs", "value"))
def callback2(tab):
if tab == "tab-1":
return tab_1
elif tab == "tab-2":
return tab_2
Resources:
- Dash tabs
- Dash Multi-Page Apps and URL Support
- Callbacks and circular imports
- Structuring a multi-tab app
- Dash clean architecture
- Plotly Dash boilerplate for multi page App
Have another approach? Let me know in the comments!
Hi!
Good setup! Do you have a Git repo with the callbacks1.py and callbacks2.py implemented? Looking for example of factoring out the majority of my primary callbacks effecting the data structures into separate files.
Thank you!