Structuring a large Dash application - best practices to follow

Hello everyone!

I’ve recently finished writing a fairly large Dash application, see here. The first time I wrote this application, I did not spend much time organizing my codebase, and thus, the code got pretty messy and unorganized. This prompted a full rewrite of the site, where I spent a large effort focusing on how to best organize the different pieces.

The following servers as a guide to highlight the structure of my newer Dash application. The ideas presented in this guide come from my own experience working as in intern in a large React project and common attributes found in open source repositories across the web.

Structure

The following is an overview of the structure. I will follow the structure from top down, covering each item and its purpose. Additionally, I’ve created this repository (https://github.com/bradley-erickson/dash-app-structure) to demonstrate the structure and serve as a template for anyone who wants to fork it. Each file in the repository includes more information about the purpose of the file.

dash-app-structure
|-- .venv
|   |-- *
|-- requirements.txt
|-- .env
|-- .gitignore
|-- License
|-- README.md
|-- src
|   |-- assets
|   |   |-- logos/
|   |   |-- css/
|   |   |-- images/
|   |   |-- scripts/
|   |   |-- favicon.ico
|   |-- components
|   |   |-- __init__.py
|   |   |-- footer.py
|   |   |-- navbar.py
|   |   |-- component1.py
|   |-- pages
|   |   |-- __init__.py
|   |   |-- complex_page
|   |   |   |-- __init__.py
|   |   |   |-- layout.py
|   |   |   |-- page_specific_component.py
|   |   |-- home.py
|   |   |-- not_found_404.py
|   |-- utils
|   |   |-- __init__.py
|   |   |-- common_functions.py
|   |-- app.py

Virtual Environment

The first few items in our structure refer to the virtual environment and package manager. This is a must for handling large applications and ensuring that packages are using the correct versions.

The .venv directory is the virtual environment itself where the project specific Python package versions are located. There are various ways to create this, but use the first command below. Note that .venv is a common name to use for your virtual environment. The requirements.txt file contains the required Python packages and their respective versions for running the application. I’ve included some additional commands for installing the current requirements, adding new packages, and updating the requirements file.

python -m venv .venv                            # create the virtual environment
.venv\Scripts\pip install -r requirements.txt   # install all packages
.venv\Scripts\pip install new_package           # install a new package
.venv\Scripts\pip freeze > requirements.txt     # update requirements with new packages

Note: there is a small shift in the Python community away from using venv and instead using pipenv. At the time of writing this, I am not as familiar with pipenv as and I am with using venv.

Environment Variables

The .env file is where you should house any passwords or keys. This is a common practice as we do not want to directly hardcode keys into your application where a malicious actor could see them. Some common values found in .env files are DATABASE_URI or API_KEY. Later on in this guide, we will see how the data is loaded.

.gitignore

The .gitignore file is specific to using Git. If you aren’t using Git, you can ignore this; however, you should be using Git or some other Version Control System. The .gitignore specifies files that should not be included when you commit or push your code.

I use the basic Python .gitignore file, located at https://github.com/github/gitignore/blob/main/Python.gitignore. Notice that both the .venv directory and .env file are included here.

License

The LICENSE file determines how your code should be shared. If you are keeping your code completely private, you do not need a license; however, you should still include one.

For more inforamtion on choosing the correct license, see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository.

README

The README.md file should specify how to install and run your application. This includes, but is not limited to, installing packages, mentioning which environemnt variables need to be set, and running the application.

src

The src directory is where to house all of the code. We will dive into each item below.

Assets

The assets directory contains css files, javascript files, locally hosted images, site logos, and the application icon. These should be organized into folders based on their purpose.

Note: some of the mentioned directories are not incldued in the sample repository as Git ignores empty directories by default.

Components

The components directory should contain common components used throughout the application. The simplest example are that of the navigation bar, navbar.py, and footer element, footer.py. All-in-one components should also be stored here. Defining each of these components in their own file is good practice. Additionally, I import each component into the __init__.py file. This allows for easier imports like the following:

from components import navbar, footer

Pages

Large structured applications rely on the new Dash Pages feature, currently only available via Dash Labs. This guide will be updated once Dash Pages is included in an official Dash release. For more information, see the community post at https://community.plotly.com/t/introducing-dash-pages-a-dash-2-x-feature-preview/57775.

The pages directory houses the individual pages of your application. I split the pages into 2 categories, static and complex pages. The static pages are ones that do not have any, or minimal callbacks, such as your home, privacy policy, about, 404 not found, or contact page. The complex pages are ones that contain more complex layouts and callbacks.

The static pages should be included immediately under the pages directory. While the complex pages should be included in their own directory inside the pages.

See the files within the complex_page directory and the pages forum post for more information about how to structure more complex pages.

Utilities

The utilities directory, utils, is meant for common functions run throughout the application. Splitting these up into specific files allows for more organized code.

In the example repository, there are 2 files, api.py and images.py.

The api.py file reads in our environment variables to get the API_KEY. The sample API called does not require a key; however, I deemed it important to include anyways. This file also defines a function that formats the inputs to call the API. To call the API, we just need to import and call the get_number_fact(int) method.

The images.py file focuses on anything to do with images shown in our application. Some of the main functionality includes reading in local images and converting them to encoded strings so they show up properly. Addtionally, if you are displaying images hosted on some Content Distribution Network (CDN), you might also define a method for formatting the url here.

App

The app.py file is the entrypoint to defining and running the application. This is where we define the Dash app, set external stylesheets, and run the app.

As it stands, Dash requires the app object for defining long_callbacks. Since this is the only place in the codebase that can access the app object, without ciruclar imports, this file should house any long_callbacks.

Closing remarks

I hope this guide helps you to build the best Dash app you can!

16 Likes

Great write up!!

1 Like

Wonderful post @raptorbrad ! I just added it to our list of tutorial posts.
I often get questions about structuring apps, especially the big ones. I’m sure this will help many community members.

Looking forward to the post on “Best Practices”

Thank you :pray:

1 Like

Hi @raptorbrad I was wondering if you have a recommendation for how to integrate testing into this structure? From reading the docs for dash.testing it seems to assume having test files at the same level as app.py. So how can we integrate this with pages, and how can we use the usual tests folder?

1 Like

I started working on some unit testing around when Dash 2.6 released; however, I ran into some import errors. I believe these have since been fixed, so its just a matter of time before someone (myself included) opens a pull request into this repo to address them.

As for testing beyond the unit variety, I don’t have the most experience with integrating these in the Dash ecosystem. Ideally tests of all varieties are kept in the /tests directory (this is in the repo already). Again, I am open to any and all Pull Requests to further improve this repo!

1 Like

Thanks so much for this @raptorbrad, my blog is quite huge and this structure will really help me out. Kudos

can anyone please share some thoughts on making an dash app as a “python package”? Thanks,

1 Like

Thank for you post ! I’ve been following it for my personal project. I have created a project on vs code windows. And now I’m going to put it on github to create a free link with render.

However there is only one concept which didn’t work with me is callback. I put all my callbacks in app.py so it’s very inelegant. Because if I put a callback in, let’s say, callback_navbar, Vs code doesn’t get the app from app.callback. However I have noticed on your projet repro that you use a navbar.py with

# component
navbar = dbc.Navbar(
    dbc.Container(
        [
            html.A(
                dbc.Row(
                    [
                        dbc.Col(html.Img(src=logo_encoded, height='30px')),
                    ],
                    align='center',
                    className='g-0',
                ),
                href='https://plotly.com',
                style={'textDecoration': 'none'},
            ),
            dbc.NavbarToggler(id='navbar-toggler', n_clicks=0),
            dbc.Collapse(
                dbc.Nav(
                    [
                        dbc.NavItem(
                            dbc.NavLink(
                                'Home',
                                href='/'
                            )
                        ),
                        dbc.NavItem(
                            dbc.NavLink(
                                'Complex Page',
                                href='/complex'
                            )
                        ),
                        html.Div(
                            login_info
                        )
                    ]
                ),
                id='navbar-collapse',
                navbar=True
            ),
        ]
    ),
    color='dark',
    dark=True,
)

# add callback for toggling the collapse on small screens
@callback(
    Output('navbar-collapse', 'is_open'),
    Input('navbar-toggler', 'n_clicks'),
    State('navbar-collapse', 'is_open'),
)
def toggle_navbar_collapse(n, is_open):
    if n:
        return not is_open
    return is_open

So I have tried it and it didn’t work for me ! I don’t know why it is a bit uncanny. Here is my navbar.py

# Dash
from dash import dcc
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc
from dash import callback, Input, Output, ctx
from dash.dependencies import Input, Output, MATCH, ALL

# components 
from components.tab_profil import profil_layout
from components.tab_recipe import recipe_layout
from components.tab_menu import menu_layout

navbar = dbc.NavbarSimple(
    children=[
        dbc.NavItem(dbc.NavLink("Profil", href="/profil")),
        dbc.NavItem(dbc.NavLink("Recipe", href="/recipe")),
        dbc.NavItem(dbc.NavLink("Menu Generator", href="/menu")),
    ],
    brand="Professor AIN V13",  # Use a relevant name here
    brand_href="/recipe",
    color="primary",
    dark=True,
)

@callback(Output('page-content', 'children'),
              [Input('url', 'pathname')])
def display_page(pathname):
    if pathname == '/recipe':
        return recipe_layout
    elif pathname == '/profil':
        return profil_layout
    elif pathname == '/menu':
        return menu_layout
    else:
        return recipe_layout