-
Notifications
You must be signed in to change notification settings - Fork 83
Getting started
This is an RStudio-internal guide to getting started with early builds of Shiny for Python.
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.
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
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.
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, theShinyApp
object needs to be assigned to theapp
variable.
One useful parameter for the ShinyApp
constructor is debug
; set it to True
to see websocket messages emitted to stdout.
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:
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.)
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"
)
)
In Shiny for Python, server functions take three arguments: input
, output
, and session
. (TODO: note types)
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.)
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({
...
})
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()
.
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)
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)
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.