Allowing users to download CSV on click

Is there a way to download data from a ‘Store’ object? All these examples seem to use a dataframe as a global variable, so the download_csv() function doesn’t have any issue pulling it into its scope.

My issue is that I have JSON data in a Store object (some people use a hidden Div) where the data will change depending on the user input. The data’s too big to just change the href of the download link, but I don’t see how I can make the @app.server.route callback aware of the Store object I am using.

Sorry if what I’m asking doesn’t make a lot of sense. Still new to this.


when users click a download button (or any other event), you need to save your data to filesystem (csv, json …) first, and then open the download url (in @app.server.route) for the users.
you can use visdcc.Run_js to write your javascript. For example, you can open download url for the users using javascript function. :slightly_smiling_face:


I am curious if this changes up when the dataframe is big. Does this work for only small dataframes or big ones as well? I have a structure, but for some reason, it works only after the first click.

@app.callback(Output(‘download-link’, ‘href’),
def download_data(starting_date,ending_date,selected_spread,selected_indicies,clicks):
if clicks > 0:
dff = df[(df[‘ReportDate’] >= pd.to_datetime(starting_date)) & (df[‘ReportDate’] <= pd.to_datetime(ending_date)) & (df[‘TickerIndex’].isin(selected_indicies))][[‘ReportDate’,‘TickerIndex’,selected_spread]].reset_index(drop=True)
downloadable_df = None
for index in selected_indicies:
temp_df = dff[dff[‘TickerIndex’]==index][[‘ReportDate’,selected_spread]].rename(columns={selected_spread:index}).reset_index(drop=True)
if downloadable_df is None:
downloadable_df = temp_df
downloadable_df = pd.merge(downloadable_df,temp_df,how=‘outer’,on=‘ReportDate’).reset_index(drop=True)
csv_string = downloadable_df.to_csv(index=False, encoding=‘utf-8’)
csv_string = “data:text/csv;charset=utf-8,” + parse.quote(csv_string)
return csv_string
return ‘’

Hey Dayxx369,

Not sure I understand your question properly, but I give it a try.
That it only happens after the first click might be a problem from n_clicks becoming clicks, then the conditional if clicks > 0, and otherwise returning an empty string which should not be recognized as a valid download link.
Hope this helps. Otherwise please try formatting your code and stripping out the relevant portion to help better.

1 Like

Wow, cannot believe that I didn’t remove that before. It works perfectly now. Thanks

dash.dependencies.Output(‘output-container’, ‘children’),
[dash.dependencies.Input(‘my-dropdown’, ‘value’)])
def update_output(value):
df1 = pd.read_csv(‘returns_summary.csv’)
df2 = pd.read_csv(‘ReturnGuideDocDetails.csv’)

if value == 'df1':
    return generate_table(df1)
elif value == 'df2':
    return generate_table(df2)

@app.callback(Output(‘my-link’, ‘href’), [Input(‘my-dropdown’, ‘value’)])
def update_link(value):
return ‘/dash/urlToDownload?value={}’.format(value)

def download_csv():
value = flask.request.args.get(‘value’)
strIO = StringIO()
strIO.write(‘You have selected {}’.format(value))

if value == 'df1':
    return send_file(strIO,
elif value == 'df2':
    return send_file(strIO,

Hey, am trying the above code but on downloading the csv, its giving the value (df1/df2 as values) instead of the csv. what do i change to download the csv ?

Hey Chris,

Is there a way to access the state of other dash components from within the routed function, without explicitly creating and managing user cookies/sessions?
I understand how to send one, two or any fixed number of variables using the routing procedure and extract them from the request, but I’ve been trying to access other information that might have flexible lengths that could exceed the maximum href length for certain browsers (e.g. large lists of vertices from lasso selection).


I tried gerdekk’s code (really helpful!) but got an issue. Here is my similar code:

app.layout = html.Div(children=[
              html.A("Générer !", href="/download_excel/")

def download_csv():
    #Convert DF
    str_io = io.StringIO()
    mem = io.BytesIO()
    return flask.send_file(mem,

This code is working well but generate a xlsx file.

Being bad in coding, I just tryed to replace “/download_excel/” (found on another topic) by “/dash/url download”, “/download” or “/dash/urlToDownload” (I tryed different things found somewhere with Google). But apparently it does not work that way :sweat_smile:

For example, “/dash/urlToDownload” leads me to with that error "AttributeError: ‘int’ object has no attribute ‘close’ ". I actually got the same error for my 3 tests.

I’m not sure how @app.server.route works…?

I assume “/download_excel/” is the reason why I got xlsx instead of csv. How could I fix that issue? Thank you!

I meant to come back to this thread based on the comment from @stu. Definitely caused some alarm discussing authentication/routing permissions. Our apps implement OAuth directly using flask-dance (instead of dash-auth), so it didn’t end up being a problem.

However, the thought about WYSIWYG, and link validity/server pressure/storage space stuck with me, along with the obvious desire to have filterable downloads (seems like people have been creating pretty complex routes!). I’ve come up with an alternative client-based solution that addresses those problems. I use it now when handling large real-time IoT data streams. I put together a basic working demo this holiday weekend, which is now running off of a free heroku instance:
Repository is on github.

Summary: the app uses FileSaverJS to create a blob out of data accessible in the browser application. It’s implemented with clientside callbacks so large data overhead doesn’t need to pass between the server and client an extra time. Particularly important when talking about large sets of processed data that might cause your worker threads to time out. Note that FileSaverJS also eliminates the browser download caps discussed in this thread.

NOTE: … older browsers may not be compatible. FileSaver requires browsers to support the Blob structure. Here’s the project’s Supported Browsers list.

In the example, data comes directly from (so… not exactly the same as saving a pandas df). It’s just grabbing the trace data from a time series scattergl trace. There’s no reason you can’t generate a blob/file directly from any object that could be passed in via callback. In some cases the data doesn’t even need to be passed in. Some examples:

  • save selected data from a figure by callback: Input('figure-id', 'selectedData') @rccg
  • directly access session storage via window.sessionStorage.getItem(..)instead of sending the contents of dcc.Store in a callback @praetor
  • If a static file really needs to be served, you could place it in a web-accessible path (e.g. /assets) and access them through an await fetch() call (warning: not secure)
# FileSaverJS supports 500MB downloads from the browser!
    ClientsideFunction('download', 'csvDownload'),
    Output('button-csv-target', 'children'),
    [Input('button-csv-download', 'n_clicks')],
    [State('btc-signal', 'figure')]

I got this to work on chrome, but for some reason it doesn’t work on Microsoft Edge. Has anyone had any luck getting it to work on Edge (or Safari, for that matter, which I haven’t checked yet?)

I also seem to be having a problem where files are being cached. When I update my code (so that the data is changed) and click the download CSV button, I get the old version of the file until I manually clear my browser’s cache. I’ve tried using Flask-Caching to clear this automatically:

cache = Cache(app.server, config={
‘CACHE_TYPE’: ‘simple’


But it doesn’t seem to be working. I am storing the data in a hidden div to share between callbacks, but I don’t think that’s what’s being cached, because one of my code changes didn’t modify the hidden div data at all, just how the hidden div data was processed before writing to csv.

EDIT: I was able to fix the caching/not updating issue by using @carlottanegri and @chriddyp’s solution. However, it still doesn’t work on Edge.

@mathbusters Are you talking about the flask-based routing that uses send_file() in python, or the FileSaverJS implementation I posted? I can’t answer your question on the flask approach, but I did check into it for the clientside javascript solution. It turns out that the current release for the Edge browser (EdgeHTML engine v18) doesn’t fully support the File API ( Specifically, it doesn’t support theFile constructor, which is used in building the CSV content prior to download.

From /assets/app-download.js#L64:

        // generate file and send through file-saver
        const file = new File([dataTable.concat("\n\n", footer)], "bpi.csv", {
            type: "text/csv;charset=utf-8"

On the other hand, the Blob object is more fully supported across browsers and versions (

I should’ve looked more deeply into it. I obviously didn’t do the requisite testing on alternate browsers (Edge isn’t used here at work). I’ll create an issue on github with a plan to migrate from using File() to Blob() on github, probably tackle it this weekend.


The demo app has been changed as I described. I tested it with Edge on a windows 10 machine (Microsoft Edge 44.17763.1.0, Microsoft EdgeHTML 18.17763)

See assets/app-download.js#L65-L70 for the relevant changes in the repo.

Where is the send_file() coming from???

Is there a way to incorporate the dcc.loading component with the app.server.route? I have a large csv file that is returned with flask.send_file but when I click the link that is passed into app.server.route there is no indication of the progress. I understand how to do this for a callback but not server.route, if it’s possible.

from flask import send_file

I’m a bit late to this but I thought’d I’d add a small snippet showing how to include multiple arguments in the href and then read those arguments to assemble your csv. In this example we need two arguments to create a dataframe, product and city. Additionally, city is a list of strings describing the cities we’ve selected.

This stackexchange post was useful in figuring out how to encode the arguments.

import io
import urllib

import flask
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Output, Input
import pandas as pd

        Output(component_id="download-link", component_property="href"),
        Input(component_id="product-dropdown", component_property="value"),
        Input(component_id="city-dropdown", component_property="value"),
def update_link(product, city):
    query_params = {"product": product, "city": city}
    query_string = urllib.parse.urlencode(query_params, doseq=True)
    return f"/dash/urlToDownload?{query_string}"

def download_csv():
    product = flask.request.args.get("product")
    city = flask.request.args.getlist("city")
    testcsv = testdf[
        (testdf["Product"] != product)
        & testdf["City"].isin(city)
    str_io = io.StringIO()

    mem = io.BytesIO()
    return flask.send_file(mem,

Another option would be to use the Download component from dash-extensions. Here is a small a data frame example,

import dash
import pandas as pd
import dash_html_components as html

from dash.dependencies import Output, Input
from dash_extensions import Download
from dash_extensions.snippets import send_data_frame

# Example data.
df = pd.DataFrame({'a': [1, 2, 3, 4], 'b': [2, 1, 5, 6], 'c': ['x', 'x', 'y', 'y']})
# Create app.
app = dash.Dash(prevent_initial_callbacks=True)
app.layout = html.Div([html.Button("Download", id="btn"), Download(id="download")])

@app.callback(Output("download", "data"), [Input("btn", "n_clicks")])
def func(n_nlicks):
    return send_data_frame(df.to_csv, "mydf.csv", index=False)

if __name__ == '__main__':

This @app.server.route(’/dash/urlToDownload’) is in a different url. To access it, just add to the end of the url. For example:

So, if you want to incorporate it with the app, you can make a button to go to that url.

I got it to work (sort of) with multiple arguments: I can download the correct csv file, and all looks good, but with debug=True it prints an error:

Callback failed: the server did not respond.

Still, it works as intended. What could this error mean?

html.A(dbc.DropdownMenuItem('Button', id='downloadButton'), id='downloadLink'),

    Output('downloadLink', 'href'),
    [Input('downloadButton', 'n_clicks'),
     Input('variable_selector', 'value'),
     Input('measure_selector', 'value'),
     Input('gender_selector', 'value'),
     Input('age_selector', 'value'),
     Input('immdes_selector', 'value'),
     Input('education_selector', 'value'),
     Input('labor_selector', 'value'),
     Input('year_selector', 'value')]
def link_download_map(click, selected_variable, selected_measure, selected_gender, selected_age, selected_heritage, selected_education, selected_labor, selected_year):
    query_params = {'variable': selected_variable,
                    'measure': selected_measure,
                    'gender': selected_gender,
                    'age': selected_age,
                    'heritage': selected_heritage,
                    'education': selected_education,
                    'labor': selected_labor,
                    'year': selected_year
    query_string = urllib.parse.urlencode(query_params, doseq=True)
    return f"/download?{query_string}" 

def download_csv():
    variable = flask.request.args.get('variable')
    measure = flask.request.args.get('measure')
    gender = flask.request.args.get('gender')
    age = flask.request.args.get('age')
    heritage = flask.request.args.get('heritage')
    education = flask.request.args.get('education')
    labor = flask.request.args.get('labor')
    year = flask.request.args.get('year')
    df_out = df[(df['variable'] == variable) & (df['year'] == int(year)) & (df['gender'] == gender) & (df['age'] == age) & (df['immdes'] == immdes) & (df['education'] == education) & (df['labor'] == labor)].copy()

    str_io = io.StringIO()
    df_out.to_csv(str_io, index=False)

    mem = io.BytesIO()
    return flask.send_file(mem,

EDIT: the error occurs with firefox, but neither chrome nor edge

As of Dash 1.20.0, we have an official dcc.Download component :tada:

This component is used just like any other Dash component :slightly_smiling_face: - Update the component’s data property with an Output and the file will be downloaded on the user’s browser.

This is a much nicer alternative than Flask’s app.server.route as it is easier to make dynamic (no query strings or flask.request.args.get!)

Thank you @Emil for contributing this component to dcc :trophy: