express.render.download

express.render.download(
    self,
    fn=None,
    *,
    filename=None,
    media_type=None,
    encoding='utf-8',
    label='Download',
)

Decorator to register a function to handle a download.

This decorator is used to register a function that will be called when the user clicks a download link or button. The decorated function may be sync or async, and should do one of the following:

  • Return a string. This will be assumed to be a filename; Shiny will return this file to the browser, and the downloaded file will have the same filename as the original, with an inferred mime type. This is the most convenient IF the file already exists on disk. But if the function must create a temporary file, then this method should not be used, because the temporary file will not be deleted by Shiny. Use the yield method instead.
  • yield one or more strings or bytestrings (b"..." or io.BytesIO().getvalue()). If strings are yielded, they'll be encoded in UTF-8. (This is better for temp files as after you're done yielding you can delete the temp file, or use a tempfile.TemporaryFile context manager) With this method, it's important that the @render.download decorator have a filename argument, as the decorated function won't help with that.

Parameters

filename : Optional[str | Callable[[], str]] = None

The filename of the download.

media_type : None | str | Callable[[], str] = None

The media type of the download.

encoding : str = 'utf-8'

The encoding of the download.

label : TagChild = 'Download'

(Express only) A label for the button. Defaults to “Download”.

Returns

:

The decorated function.

See Also

Examples

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400

## file: app.py
import asyncio
import io
import os
from datetime import date

import matplotlib.pyplot as plt
import numpy as np

from shiny.express import render, ui

ui.page_opts(title="Various download examples")

with ui.accordion(open=True):
    with ui.accordion_panel("Simple case"):
        ui.markdown("Downloads a pre-existing file, using its existing name on disk.")

        @render.download(label="Download CSV")
        def download1():
            """
            This is the simplest case. The implementation simply returns the name of a file.
            Note that the function name (`download1`) determines which download_button()
            corresponds to this function.
            """

            path = os.path.join(os.path.dirname(__file__), "mtcars.csv")
            return path

    with ui.accordion_panel("Dynamic data generation"):
        ui.markdown("Downloads a PNG that's generated on the fly.")

        ui.input_text("title", "Plot title", "Random scatter plot")
        ui.input_slider("num_points", "Number of data points", min=1, max=100, value=50)

        @render.download(label="Download plot", filename="image.png")
        def download2():
            """
            Another way to implement a file download is by yielding bytes; either all at
            once, like in this case, or by yielding multiple times. When using this
            approach, you should pass a filename argument to @render.download, which
            determines what the browser will name the downloaded file.
            """

            print(input.num_points())
            x = np.random.uniform(size=input.num_points())
            y = np.random.uniform(size=input.num_points())
            plt.figure()
            plt.scatter(x, y)
            plt.title(input.title())
            with io.BytesIO() as buf:
                plt.savefig(buf, format="png")
                yield buf.getvalue()

    with ui.accordion_panel("Dynamic filename"):
        ui.markdown(
            "Demonstrates that filenames can be generated on the fly (and use Unicode characters!)."
        )

        @render.download(
            label="Download filename",
            filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100, 999)}.csv",
        )
        async def download3():
            await asyncio.sleep(0.25)
            yield "one,two,three\n"
            yield "新,1,2\n"
            yield "型,4,5\n"

    with ui.accordion_panel("Failed downloads"):
        ui.markdown(
            "Throws an error in the download handler, download should not succeed."
        )

        @render.download(label="Download", filename="failuretest.txt")
        async def download4():
            yield "hello"
            raise Exception("This error was caused intentionally")


## file: mtcars.csv
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb
21,6,160,110,3.9,2.62,16.46,0,1,4,4
21,6,160,110,3.9,2.875,17.02,0,1,4,4
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4
10.4,8,460,215,3,5.424,17.82,0,0,3,4
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6
15,8,301,335,3.54,3.57,14.6,0,1,5,8
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2