Learn Shiny for Python with a Puppy Traits Dashboard
Exploring Shiny for Python With A Puppy Traits Web Application

Shiny has been revered for over a decade within the R-ecosystem, and recently all its goodness was introduced to Python as well. Shiny is a web application framework that can be used to create interactive web applications that can run code in the backend.
I have been an avid user of R Shiny for years, so I was naturally excited when it was introduced to Python as well. In this article, I will walk through the steps I took to create my "Who is the Goodest Doggy" application , from foundations to styling. Let's get started!
Code and data availability
Code for recreating everything in this article can be found in my GitHub repo.
Data: This article uses synthetic data with fake dog trait ratings generated by me. The synthetic data is available at the linked GitHub page and was inspired by the data I found on TidyTuesday, courtesy of KKakey and originally sourced from true sightings at American Kennel Club.
Data generation process: To generate the data, I took the unique dog and trait combinations and used numpy
library to generate a random rating between 2–5 for each breed and characteristic. The full code to generate this dataset can be found at the linked repo within the data folder.
Setting up the Python environment
- Install Shiny for Python extension (For VSCode)
- Create a virtual environment for your project (good practice)
- Install the required packages by either running individual commands like "pip install shiny" or installing all packages by running "pip install -r requirements.txt".
Base components of Shiny
This foundation command is my favorite because the Posit team has made it very easy to get up and running with base Shiny templates.
Shiny create --help

This command opens up the options for the various Shiny templates. Every time I check there is a new template on this list as it is being developed very fast. The image above shows what appears when the command is run. I will pick the basic-app template and run the command shiny create -t basic-app.
This command created the app template with a simple app.py
file that generated the Shiny dashboard below when run. In this app, the user can choose a number on the scrollbar, and the app updates the calculation and written text underneath it.

Let's take a look at the app.py
file contents:
from shiny import App, render, ui # Import required packages
app_ui = ui.page_fluid(
ui.panel_title("Hello Shiny!"), # Create the panel for title
ui.input_slider("n", "N", 0, 100, 20), # Create the slider
ui.output_text_verbatim("txt"), # Create space for text on the UI
)
def server(input, output, session):
@render.text # Decorator to define output format
def txt(): # The function that will create the text to send over to the ui
return f"n*2 is {input.n() * 2}" # The text calculation
app = App(app_ui, server)
Similar to Shiny for R, there is a ui
and server
component within the app.py file. The ui
contains the frontend of the application and the server
logic handles the backend. I will update this template for exploring puppy traits dataset.
Personalizing the application
Adding dataframe and plot outputs
After choosing the base template, I will update the app.py
file for my data and analysis needs. To output a dataframe and a plot in my application, I can update the ui
and server
logic to read and display the dataframe as a pandas object and call a plotting function on the server side.
from shiny import App, render, ui
import pandas as pd # Import pandas for working with data frames
from pathlib import Path
from trait_rating_plot import create_trait_rating_plot # Import plot function
# Read the dataframe
df = pd.read_csv(Path(__file__).parent / "dog_traits.csv", na_values = "NA")
# UI logic
app_ui = ui.page_fillable(
ui.output_data_frame("dog_df"), # Create space for plot output
ui.output_plot("breed_plot") # Create space for dataframe output
)
# Server logic
def server(input, output, session):
@render.data_frame # Decorator to define output structure for data frame
def dog_df():
return df
@render.plot # Decorator to define output structure for plot
def breed_plot():
fig = create_trait_rating_plot(df, "Bulldogs") # Call external function to generate the plot for Bulldogs
return fig
app = App(app_ui, server)
The above code creates the Shiny output shown below. Since it is neither pretty nor dynamic yet, there is quite some work to be done. For now this is a static output. The dataframe output shows the entire dataset. The plot output is hardcoded for "Bulldogs". Their placement it simply one followed by another as the application is not yet structured with its layout.

Adding dynamic components: Sidebar layout and filters
Sidebar
This is one of the most common layouts I have seen on Shiny applications. In the above code, I will encompass the ui
components into a ui.page_sidebar()
function. This function has two parts:
- The first part sits inside
ui.sidebar()
function. Allui
components that need to show up in the side panel are coded within this function. - The second part is everything else, which sits in the main panel area.
Filters inside the sidebar panel
Now that I have my sidebar, I will add a user input dropdown filter for the dog breed using the ui.input_select()
function so that my plot is not hardcoded for "Bulldogs", and can be updated for any dog.
I will also add a multi-selection filter for dog characteristics that the user is interested in. This will only update the dataframe to show the characteristics selected in order to compare them amongst dogs.
Updated ui
logic:
# Read the dataframe
df = pd.read_csv(Path(__file__).parent / "dog_traits.csv", na_values = "NA")
breeds = df.breed.unique().tolist()
traits = df.trait.unique().tolist()
app_ui = ui.page_fillable(
ui.page_sidebar( # Sidebar layout
ui.sidebar( # First part - sidebar panel
ui.input_select("inputbreed", # Single input filter for dog breed
label = "Select breed",
choices = breeds,
selected="Bulldogs"),
ui.input_selectize(id = "inputtrait", # Multi input filter for characteristics
label= "Select traits",
choices = traits,
multiple=True,
selected="Adaptability Level"),
),
ui.output_data_frame("dog_df"), # Main panel
ui.output_plot("breed_plot") # Main panel
),
)
On the server side:
- I will update the plot output function to take the user input breed instead of default "Bulldogs". This makes the plot dynamically react to the dog breed input.
- I will update the dataframe output to add the characteristics filtering logic based on user selection. This makes the dataframe update dynamically for the selected characteristics by the user.
def server(input, output, session):
@render.data_frame
def dog_df(): # Filter and sort based on user characteristics selection
filtered_df = df[(df['trait'].isin(input.inputtrait()))]
return filtered_df.sort_values(by=["trait", "rating"], ascending=[True, False])
@render.plot
def breed_plot(): # filter and sort based on user breed selection
df_updated = df.copy()
fig = create_trait_rating_plot(df_updated, input.inputbreed())
return fig
As in the image below, there is now a sidebar layout with the filters sitting in the side panel and everything else in the main panel.

Updating multiple outputs with "reactive calculations"
So far, I added two filters both of which impact only one of the outputs. I also want to add slider inputs for minimum and maximum ratings that the user wants to see reflected in both the data frame and the plot outputs. In this scenario, I will create slider inputs on the ui
side for the user to select rating limits.
ui.input_slider(id = "ratingmin", # Slider input for minimum rating
label="Minimum rating",
min=1, max=5, value=1),
ui.input_slider(id = "ratingmax", # Slider input for maximum rating
label="Maximum rating",
min=1, max=5, value=5),
I will then create a new table output on the server
side that filters the whole table based on the rating limits selected. This is called a "reactive calculation" and hence it sits under the decorator reactive.Calc
.
I will use this new reactive calculation output as an input to the other two server functions for my dataframe and plot, so both of them get impacted by the slider inputs. The output of a reactive calculation can be accessed in other server functions using reactive brackets.
def server(input, output, session):
@reactive.Calc # New reactive calculation for filtering data by rating
def filtered_ratings():
filtered_rating = df[(df['rating'] >= input.ratingmin()) &
(df['rating'] <= input.ratingmax())]
return filtered_rating.sort_values(by=["trait", "rating"], ascending=[True, False])
@render.data_frame # updated to use "filtered_ratings()" instead of original df.
def dog_df():
filtered_df = filtered_ratings()[(filtered_ratings()['trait'].isin(input.inputtrait()))]
return filtered_df.sort_values(by=["trait", "rating"], ascending=[True, False])
@render.plot # updated to use "filtered_ratings()" instead of original df.
def breed_plot():
fig = create_trait_rating_plot(filtered_ratings(), input.inputbreed())
return fig
Updated application output:

Adding "Apply" settings button
The concern with having multiple filters is that every time one of them is updated, the application gets updated immediately instead of waiting for the user to update every filter they need to change before refreshing the application. I will add an Apply
button that will allow the outputs to get updated based on the selected filters once everything has been inputted and the button is clicked. The button is added within the ui
logic as below:
ui.input_action_button("apply", # id of the button to access on the server side
"Apply settings", # Externally visible label
class_="btn-secondary") # The color
On the server
side, I will update the dataframe and plot output functions to become "reactive events"
, so that they will react only when "Apply"
is pressed.
@render.data_frame
@reactive.event(input.apply, ignore_none=False)
def dog_df():
filtered_df = filtered_ratings()[(filtered_ratings()['trait'].isin(input.inputtrait()))]
return filtered_df.sort_values(by=["trait", "rating"], ascending=[True, False])
@render.plot
@reactive.event(input.apply, ignore_none=False)
def breed_plot():
fig = create_trait_rating_plot(filtered_ratings(), input.inputbreed())
return fig
When the application is run, there should be an additional "Apply" button along other filters.
Now, I will finally work to make the app look more presentable!
UI layouts
From here, I will work to update only the ui
components that will reflect on the frontend.
Adding a boundary around the plots and updating to column layout
I will now add a boundary around both my dataframe and plot by encompassing each of those elements within ui.card()
function. This function also allows adding a heading by using ui.card_header()
within the ui.card()
function.
I will then encompass both of these ui
elements within the ui.layout_columns()
function so that I can assign them designated column space that I want each ui
component to occupy. There are total 12 units I can assign to the components within a column layout. I will assign the dataframe and plot a space of 5 and 7 units respectively.
ui.layout_columns( # Encompass all ui components within the main panel into column layout
ui.card( # Encompass dataframe space within ui.card to create boundary
ui.card_header("Select the traits to update this plot"), # Card title
ui.output_data_frame("dog_df"),
),
ui.card( # Encompass plot space within ui.card to create boundary
ui.card_header("Select the breed to update this plot"), # Card title
ui.output_plot("breed_plot")
),
gap = "2rem", # Column layout function input for gap between columns
col_widths={"sm": (5, 7)}, # Size of the columns for each component
height = "400px" # Height of the columns
)
Updated application output:

Looks so much cleaner already.
Images, texts and panels
Next, since this is a puppy dashboard, I want to add an irresistible puppy image at the top of my dashboard side by side with the heading. For these two to be beside each other at the top of the dashboard, I will add a row above my plot and dataframe section. Within this row I will create another column layout and use the following functions.
ui.tags.img()
: I will use this function to add an image within this layout.ui.tags.h1()
: I will use this function to add the dashboard heading. There are a lot of other capabilities of ui.tags that are available in the Shiny documentation.ui.markdown()
: I will add a data source call out using simple text with this function.ui.panel_absolute()
: I will add the dashboard heading and markdown text within a panel using this function that stays visible while scrolling the dashboard.ui.panel_conditional()
: I will create a conditional panel for my ratings filter, so they are visible only when the checkbox to "Set limits for ratings" is selected.
# Puppy image url
dogimg_url = "https://images.unsplash.com/photo-1444212477490-ca407925329e?q=80&w=1856&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
# Row layout
ui.row(
# Multiple columns within each row
# First column for the image using ui.tags.img
ui.column(6, ui.tags.img(src=dogimg_url, height="90%", width="100%")),
# Second column for the dashboard heading and data source call out
ui.column(5,
ui.panel_absolute(
ui.panel_well(
ui.tags.h1("Who is the goodest doggy?!?"),
ui.markdown("Data: Synthetic dog trait ratings inspired by TidyTuesday dataset courtesy of [KKakey](https://github.com/kkakey/dog_traits_AKC/blob/main/README.md) sourced from the [American Kennel Club](https://www.akc.org/)."),
ui.markdown("Photo by [Anoir Chafik](https://unsplash.com/@anoirchafik?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) on [Unsplash](https://unsplash.com/photos/selective-focus-photography-of-three-brown-puppies-2_3c4dIFYFU?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)")
),
width="450px",
right="75px",
draggable=False,
)
)
)
Code for the conditional panel:
ui.input_checkbox("show", "Set limits for ratings", False), # Create a checkbox for conditional input
ui.panel_conditional( # Only show the components within this function when condition is true
"input.show", # Condition that the checkbox is selected
ui.input_slider(id = "ratingmin", label="Minimum rating", min=1, max=5, value=1),
ui.input_slider(id = "ratingmax", label="Maximum rating", min=1, max=5, value=5),
)
Updated application:

Themes
Color schemes and themes are so important, because I think they define the personality and branding of the dashboard. I will update the background of my sidebar panel as well as the theme of the entire dashboard.
- Sidebar panel color: Within the
ui.sidebar()
function I will use"bg"
input to provide the background color. I will also provide it the input"open"
so that the sidebar is open by default, and the user has an option to collapse it and reopen it. - I will use
shinyswatch
package to import themes for the dashboard. There are multiple themes available through this package but for this dashboard I lovedtheme.minty()
.
Updated ui
code:
from shinyswatch import theme # import the package for various prebuilt themes
....
app_ui = ui.page_fillable(
theme.minty(), # Call minty() theme to update the page
...
ui.sidebar( # Sidebar panel
...,
bg="#f6e7e8", open="open" # Panel variable inputs
)
)
The final application
The application is now ready with all the elements that I added step by step. Here is the final output and the end to end code to reproduce it is in the GitHub repo.

Why is now a great time to get started with Shiny for Python
I think a few reasons make right now the perfect time to enter this space.
- Shiny for Python is just getting started. If R Shiny is any indicator, there is a lot more coming and Posit has been moving very quickly to incorporate python in their ecosystem.
- There are new applications coming up like Shiny Express that are making Shiny for Python even more accessible for quick prototypes.
- If you come from a Shiny for R background, core Shiny for Python is a great step into the world of Python.
- If you come from a Streamlit for Python background, Shiny express provides that perfect balance between Streamlit's agility for prototyping and Shiny's efficiency.
I hope you found this article encouraging to go learn and build Shiny for Python web applications. Keep an eye out for the next article in my Shiny series on Python Shiny Express, the newest kid on the block.
Code for recreating everything in this article can be found at https://github.com/deepshamenghani/shinypython_meetup/.
If you'd like, find me on Linkedin.