rguides

Shiny for Python Developers

If you already build interactive apps with Python frameworks like Streamlit or Gradio, you have a huge advantage when learning Shiny for R. The mental model is similar, but the syntax and architecture differ in important ways. This tutorial maps your existing Python knowledge to Shiny concepts, so you can start building right away.

What you’ll learn

This tutorial covers the key concepts and practical techniques for working with Shiny for Python Developers. By the end, you will know how to apply the core functions in real data analysis workflows.

The mental model: what carries over

As a Python developer, you’re used to defining UI and server logic, handling user inputs, and automatically updating outputs when values change. Shiny follows the same pattern, but with R syntax.

What stays the same:

  • You define inputs (sliders, text boxes, dropdowns) and outputs (plots, tables, text)
  • Changing an input automatically updates dependent outputs
  • You run the app and view it in a browser

What changes:

  • The language is R, not Python
  • The architecture separates UI and server more explicitly
  • Reactivity uses a different mechanism than Streamlit’s @st.fragment or Gradio’s fn

Your first Shiny app

Here’s a simple Shiny app that displays a histogram based on user input:

library(shiny)
library(ggplot2)

ui <- fluidPage(
  titlePanel("Histogram Builder"),
  sidebarLayout(
    sidebarPanel(
      sliderInput("bins", "Number of bins:", 
                  min = 5, max = 50, value = 20)
    ),
    mainPanel(
      plotOutput("histogram")
    )
  )
)

server <- function(input, output, session) {
  output$histogram <- renderPlot({
    ggplot(faithful, aes(x = waiting)) +
      geom_histogram(bins = input$bins, fill = "steelblue") +
      labs(x = "Waiting time (minutes)", y = "Count")
  })
}

shinyApp(ui = ui, server = server)

Output: A browser window opens showing a histogram with an interactive slider.

Compare this to Streamlit:

import streamlit as st
import pandas as pd
import ggplot

bins = st.slider("Number of bins", 5, 50, 20)
st.histogram(faithful, column="waiting", bins=bins)

The key difference: Streamlit uses decorators and executes top-to-bottom on each interaction. Shiny uses a reactive graph where you explicitly declare dependencies.

Inputs and outputs: streamlit vs Shiny

Here’s a side-by-side comparison of common input widgets:

Python (Streamlit)R (Shiny)
st.slider("Label", min, max, default)sliderInput("id", "Label", min, max, default)
st.text_input("Label", "default")textInput("id", "Label", value = "default")
st.selectbox("Label", options)selectInput("id", "Label", choices = options)
st.checkbox("Label")checkboxInput("id", "Label")

In Shiny, every input gets an ID (like "bins"). You access it in the server as input$bins. Outputs also get IDs, and you render them with render* functions:

output$plot <- renderPlot({ ... })
output$table <- renderTable({ ... })
output$text <- renderText({ ... })

Reactivity: the critical difference

Streamlit reruns your entire script (or decorated function) when widgets change. Shiny’s reactivity is more granular and efficient.

In Shiny, reactive expressions only recalculate when their dependencies change:

server <- function(input, output, session) {
  
  # This only runs when input$n changes
  filtered_data <- reactive({
    iris[1:input$n, ]
  })
  
  # This runs when filtered_data() changes
  output$summary <- renderPrint({
    summary(filtered_data())
  })
  
  # This also runs when filtered_data() changes
  output$table <- renderTable({
    filtered_data()
  })
}

Notice the parentheses: filtered_data() is a function call. In Shiny, reactive expressions are functions—you must call them with () to get the value.

This is different from Gradio, where your function receives inputs directly:

def predict(text):
    return model(text)

In Shiny, you access inputs via the input object and wrap outputs in render* functions.

Going further

This tutorial covered the fundamentals. To build production-quality apps, explore these topics:

Reactive vs imperative

Python developers used to Django or Flask expect imperative request-response code: when a user clicks, run this function, update this state, return this response. Shiny is declarative and reactive: define what each output depends on, and Shiny handles when to recompute. output$plot <- renderPlot({ ggplot(filter_data(input$category)) }) declares that output$plot depends on input$category and a filtered dataset, Shiny redraws the plot automatically whenever either changes.

Python Shiny

shiny is available as a Python package. The API mirrors the R Shiny API: @app.ui defines the UI with ui.page_fluid(), ui.input_slider(), etc. @app.server defines the server function. The reactive primitives, reactive.Calc, reactive.Value, @reactive.Effect, correspond to reactive(), reactiveVal(), and observe() in R. Python Shiny apps can use any Python plotting library: matplotlib, plotly, or altair.

Data sharing between languages

For data-intensive Python Shiny apps, keep data processing in Python where your existing tools are. For R developers building on a Python Shiny backend, reticulate runs R code within a Python process. The more practical approach is usually to pre-process data in R, save as Parquet, and load in Python Shiny, separating the analysis (R) from the web app (Python Shiny) at the file boundary.

Deployment

Python Shiny apps deploy to Posit Connect, ShinyApps.io, and any platform that supports ASGI Python web apps (Starlette under the hood). Docker deployment uses the ghcr.io/posit-dev/py-shiny base image. For R Shiny apps, use rocker/shiny instead. The infrastructure is the same; only the base image changes.

Shiny vs Python web frameworks

Python web developers typically know Flask, Django, or FastAPI. Shiny occupies a different niche, it is an analytical web framework designed for data scientists, not web developers. The core difference: Shiny applications are reactive computation graphs, not request-response handlers.

In Flask, a route receives a request and returns a response. In Shiny, inputs trigger reactive recalculations, and outputs automatically update when their dependencies change. There are no explicit request handlers — the reactive system manages data flow. This model maps naturally to data analysis workflows where changing one parameter should automatically update all related charts and tables.

Shiny for Python (shiny package) brings this model to Python. R Shiny and Python Shiny use similar syntax: @render.plot in Python corresponds to renderPlot() in R, and @reactive.calc corresponds to reactive(). The concepts translate directly.

Reactive programming concepts

The three reactive building blocks: reactive inputs (widgets), reactive expressions (computed intermediates), and reactive outputs (rendered results).

In Python Shiny: input.my_slider() reads an input value. @reactive.calc def filtered_data(): return df[df.value > input.threshold()] creates a cached reactive expression. @render.table def my_table(): return filtered_data() renders an output. When input.threshold changes, filtered_data() is invalidated, causing my_table to re-render.

The reactive graph ensures that recalculations happen exactly when needed, not more. If two outputs share a reactive expression, changing an input recalculates the shared expression once, not twice. This automatic deduplication is a major advantage over manual dependency management.

UI components

Shiny’s UI is declarative HTML. ui.input_slider("id", "Label", min=0, max=100, value=50) creates a slider. ui.output_plot("chart_id") reserves space for a chart. The UI hierarchy uses containers: ui.page_sidebar(), ui.card(), ui.layout_columns().

For R Shiny, the pattern is similar: sliderInput("id", "Label", min=0, max=100, value=50) in the UI, output$chart_id <- renderPlot({...}) in the server. The conceptual model is identical; the syntax differs.

Migrating from dash or streamlit

Python data scientists familiar with Dash or Streamlit will find Shiny’s mental model different. Streamlit re-runs the entire script on each interaction — simple to reason about but less efficient. Dash uses explicit callback decorators mapping inputs to outputs. Shiny’s reactive graph is automatic: declare dependencies in code and Shiny tracks them.

The learning curve for Shiny is steeper than Streamlit but the resulting apps are more maintainable for complex use cases. Shiny’s reactive caching prevents redundant computation; Streamlit’s full re-execution can be slow for computationally intensive apps.

From R Shiny to Python Shiny, the migration is mostly syntax: renderPlot({}) becomes @render.plot def ..., reactive({}) becomes @reactive.calc def .... The shiny.express module provides a Streamlit-like script interface for simpler apps.

Data integration patterns

Shiny apps typically load a dataset once at startup and make it available to all reactive contexts. In Python Shiny: load data outside the server function (module level). In R Shiny: load data outside server and ui (top-level script).

For data that changes during the app’s lifetime, store it in a reactive value: data_store = reactive.Value(initial_df) in Python or rv <- reactiveValues(data = initial_df) in R. Updating the reactive value triggers all dependent outputs to refresh.

Database queries inside reactive contexts run on each invalidation. Cache expensive queries with @reactive.calc (Python) or reactive() (R) — Shiny only re-runs the query when its inputs change, not on every render cycle.

Key takeaways

As a Python developer, you already understand the mental model. The main adjustments are:

  1. R syntax over Python, same patterns, different language
  2. Explicit UI/server separation, your UI and server are distinct objects
  3. Reactive graph, not top-down execution, outputs depend on specific inputs, not the whole script
  4. Call reactive expressions as functions, use () to access their values

Once these click, you’ll find Shiny’s reactive system actually gives you more control than Streamlit’s automatic reruns. Happy building!

Next steps

Now that you understand shiny for python developers, explore these related topics to deepen your knowledge and apply these techniques in more complex scenarios.