Skip to content

Getting started

Joe Cheng edited this page Feb 10, 2022 · 7 revisions

This is an RStudio-internal guide to getting started with early builds of Shiny for Python.

Intended audience

This guide assumes you already know how to build apps using Shiny for R, and you also already know Python. (Future public-facing docs will not assume you already know Shiny for R.)

It is only intended to get you writing basic Shiny apps; our current goal is to get feedback about how the API feels, especially to experienced Python folks.

Prerequisites

First, create a new directory for your first Shiny app, and change to it.

mkdir my-shiny-app
cd my-shiny-app

If you want to use a virtual environment, feel free to create/activate one now:

# Create a virtual environment in the .venv subdirectory
python3 -m venv .venv

# Activate the virtual environment
source .venv/bin/activate

You'll need the shiny and htmltools packages, which are not yet on PyPI. Currently, the easiest way to install is to run the following:

curl https://rstudio.github.io/prism/requirements.txt > requirements.txt
python3 -m pip install -r requirements.txt

Running

Create an app.py file and paste this in:

from shiny import *

app_ui = ui.page_fluid(
    ui.input_slider("n", "N", 0, 100, 20),
    ui.output_text_verbatim("txt", placeholder=True),
)


def server(input: Inputs, output: Outputs, session: Session):
    @reactive.calc()
    def r():
        return input.n() * 2

    @output()
    @render_text()
    async def txt():
        val = r()
        return f"n*2 is {val}, session id is {session.id}"


app = App(app_ui, server)

Take a moment to really study the above app.py code. If you're already very comfortable with Python and Shiny for R, you can probably guess what's happening here.

To run the app, run this command from the shell, in the same directory as app.py:

shiny run --reload

This should start your app and automatically launch a web browser as well.

The --reload flag means that file changes in the current directory tree will cause the Python process to reload, so your workflow is 1) save changes to app.py, 2) reload web browser.

App basics

Like Shiny for R, Shiny for Python apps are composed of two parts: a app_ui (i.e., ui) object and a server function. These are then combined using a ShinyApp object.

Δ: In R, the shinyApp() isn't assigned to a variable, it just needs to be the last expression on the page. In Python, the ShinyApp object needs to be assigned to the app variable.

One useful parameter for the ShinyApp constructor is debug; set it to True to see websocket messages emitted to stdout.

User interface

Shiny for Python uses a UI paradigm that's carried over mostly intact from R. It's based on a Python port of htmltools. The two main differences are:

Naming conventions

In R, you say fluidPage; in Python, it's ui.page_fluid. In R, you say selectInput; in Python, it's ui.input_select. (The reversing of the order of adjective and noun is something we would do in R as well, if we could start from scratch today.)

Passing children

Like in R, htmltools treat keyword (i.e. named) arguments as attributes:

>>> from shiny.ui import *
>>> a(href="help.html")
<a href="help.html"></a>

And positional (i.e. unnamed) arguments as children:

>>> tags.span("Hello", strong("world"))
<span>
  Hello
  <strong>world</strong>
</span>

Sadly, when calling a function, Python requires all keyword arguments to come after all positional arguments--exactly the opposite of what we would prefer when it comes to HTML.

This works, but it's a little awkward that the href and class are stacked at the end instead of the beginning:

>>> a(span("Learn more", class_ = "help-text"), href="help.html")
<a href="help.html">
  <span class="help-text">Learn more</span>
</a>

And you can imagine how much worse it would be for, say, a call to page_navbar() (i.e., navbarPage()) with many lines of code inside nav() (i.e., tabPanel()) children, and then the page's keyword arguments (e.g., title) appearing only at the very end.

As a workaround, there's a second way to place attributes before children: provide a dictionary of attributes to the positional arguments of a HTML tag-like function.

a({"href": "help.html"},
  span({"class": "help-text"},
    "Learn more"
  )
)

Server logic

Function signature

In Shiny for Python, server functions take three arguments: input, output, and session. (TODO: note types)

Accessing inputs

input.x() is equivalent to input$x.

Note that unlike in R, the () is necessary to retrieve the value. This aligns the reading of inputs with the reading of reactive values and reactive expressions. It also makes it easier to pass inputs to module server functions.

If you need to access an input by a name that is not known until runtime, you can do that with [:

input_name <- "x"
input[input_name]()  # equivalent to input["x"]()

(We don't currently have a supported/documented way of having type hints when accessing inputs, but we're working on it.)

Defining outputs

Define a no-arg function whose name matches a corresponding outputId in the UI. Then apply a render decorator and the @output() decorator.

@output()
@render_plot()
def plot1():
    np.random.seed(19680801)
    x = 100 + 15 * np.random.randn(437)

    fig, ax = plt.subplots()
    ax.hist(x, input.n(), density=True)
    return fig

(Note: The order of the decorators is important! We need the output() to be applied last, and in Python, decorators are applied from the bottom up.)

This is equivalent to this R code:

output$plot1 <- renderPlot({
  ...
})

Creating reactive expressions

Reactive expressions (i.e., shiny::reactive()) are created by defining (no-arg) functions, and adding the @reactive.calc decorator.

@reactive.calc()
def foo():
    return input.x() + 1

You access the value of this foo reactive by calling it like a function: foo().

Creating observers

Observers (i.e., shiny::observe()) are created by defining (no-arg) functions, and adding the @reactive.effect decorator.

@reactive.effect()
def _():
    print(input.x() + 1)

Reactive values and isolation

A reactive value (i.e., shiny::reactiveVal()) is initialized with reactive.value():

x_plus_1 = reactive.value()

And similar to shiny::isolate(), with isolate() can be used to read/update reactive values without invalidating downstream reactivity.

@reactive.effect()
def _():
    x = input.x()
    with isolate():
       x_plus_1(x + 1)

Events

To delay the execution of a reactive expression until a certain event occurs (i.e., shiny::bindEvent() in R), add the @event decorator and provide to it a callable function.

@reactive.effect()
@event(input.btn)
def _():
    print("input_action_button('btn') value: ",  str(input.btn()))

Note that there is no direct translation of eventReactive() or observeEvent() in Python since @effect() can be applied to either @reactive.calc() or @reactive.effect() in such a way that it removes the need for having them.