Let’s Build a Basic Python Shiny App

Tutorial
Python Shiny
An interactive tutorial allowing newcomers to Shiny to build a basic application.
Author

Rich Leyshon

Published

July 20, 2023

Getting to grips with Python Shiny.

Woman Instructing On Laptop.
Source: https://www.wallpaperflare.com/. Creative Commons License.

This tutorial is intended for those who are already familiar with Python, but may be less familiar with dashboarding and Shiny in Python. It may also be of interest to those who are well-versed in RShiny and would like to see how it has been implemented in Python.

This is a light-weight, introductory Python Shiny tutorial. No installation of software is required, other than a web-browser (which you must already have) and a willingness to experiment. We will use the shinylive service to display the application that we write and steadily add to a basic app, discussing some of the concepts as we go. Finally, let’s regroup and reflect on some coping techniques for when you begin writing your own Python Shiny apps.

This tutorial will not attempt to reproduce any of Posit’s documentation, which is rather excellent so please check that out. Also, if you would prefer a conceptual treatment of Python Shiny, please see my blog on The Current Stateof Python Shiny.

How to…

Feel free to tinker with the code in the following example apps and then press play in the top-right hand corner of the console interface. Don’t worry - you won’t break anything important. To reset the code, simply refresh your web page.

If the app doesn’t launch, you’ll see some spinning grey hexagons that never go away . This is likely to be a problem with permissions in your browser. But you can click on the collapsible code block below the app windows and copy the code to an app.py file on your computer. If you have python and python shiny installed, you should be good to go.

Hello Python Shiny!

Below is a really minimal app that doesn’t do very much at all. The python code is presented on the left. The interactive app is presented on the right. You can type into the app’s text field, and that’s about it for now.

Show the code
#1 Import modules to help us build our app
from shiny import ui, App

#^putting things above the app means they can be shared between your ui and server

app_ui = ui.page_fluid(
  #2 all of the elements you wish to show your user should go within the ui
    ui.input_text(
        id="name_entry",
        label="Please enter your name",
        placeholder="Your name here"
        )
)

def server(input, output, session):
  #3 this is where your app logic should go. So far, not much...
    return None
  
# Finally - this bit packages up our ui and server. Super important - it must
# be named `app`.

You’ll see that the code defines an app_ui object, which is a Shiny ui instance. Within that ui.page_fluid() function, we can specify all the elements of the app that we would like to present to our users.

On Users…

There are only two industries that call their customers “users”: illegal drugs and software – Edward Tufte

So far, only one simple ui element has been defined. The humble text input ui.input_text() which allows our users to place their own text into a text field.

Notice that in Python, all the inputs begin with input.... There’s ui.input_text() as we’ve seen, but there’s lots more. ui.input_date(), ui.input_file() and ui.input_slider to name a few. This consistent syntax approach is a subtle improvement over RShiny and makes it so much easier to work with the vast array of widgets without having to remember them all. If you’re working in a modern editor such as Visual Studio Code, simply typing ui.input will remind you of all the options available to you. For those not working in a nice GUI like VSCode, a Shiny cheatsheet may be useful, though note that at the time of writing I could only find R-flavoured ones…

All ui input elements start with the same 2 arguments, id and label:

  • id: The internal name of the input. What does that mean? Call it what you like. Whatever you choose, be aware that when you want to use the values from the text input to do something in the server, it’s this name that you will need to reference.
  • label: A short piece of text that prompts your user to do something. This will be displayed in your ui above the input element.

Doing Something With the Shiny Server.

Unfortunately, so far our app doesn’t actually do much. Typing into the empty text field yields no result. That’s because right now, our server function simply returns None. Let’s resolve this.

Show the code
#1 update the import statement to include `render` module
from shiny import ui, App, render

app_ui = ui.page_fluid(
    ui.input_text(
        id="name_entry",
        label="Please enter your name",
        placeholder="Your name here"
        ),
    #2 Include a ui output element. This will show the calculations
    # made in the server back to the user.
    ui.output_text_verbatim("greeting"),
)

def server(input, output, session):
    #3 Update the server with a function that handles the text response.
    @output # use the output decorator to mark this function
    @render.text # also need to ensure we use the correct render decorator
    def greeting():
        """
        This function will take the output of the ui.input_text() element,
        format the string in a polite sentence and format it as an HTML
        output for the ui to show.
        """
        return f"Hi {input.name_entry()}, welcome to Python Shiny."

app = App(app_ui, server)

There’s quite a lot going on in the above code chunk. Let’s start with the decorators @output & @render.text:

  • @output: Any function marked with this decorator will have its returned value made available to the user interface. Notice that in the line ui.output_text_verbatim("greeting") we are able to call on the values of the server’s greeting() function that we marked as an @output.
  • @render.text: This tells Shiny what type of output to handle. Is it text, a chart (@render.plot) or something more fancy, like dynamically rendered ui (@render.ui). These output types all have their corresponding output functions to use in the ui. Here we called ui.output_text_verbatim().
  • Calling the wrong ui-side function may not result in an error, but can have unexpected results, such as your text disappearing from your app. Keep an eye out for that if things aren’t working - are you using the correct combination of render in the server with output_... in the ui?

Did you notice anything off-putting about the above code? Yes, too many comments but please indulge me. Functions in the server and ui are passing values back and forth. That can be a bit overwhelming to get your head around when you’re new to what’s known as ‘event-driven programming’. All that means is that the program needs to respond to some action taken by the user. The syntax in which you reference the functions is a bit inconsistent to my mind. Let’s take a closer look:

If I mark some function make_plot() in the server as an @output and then wish to call on its value within the ui, I need to use ui.output_plot("make_plot"). Notice the lack of brackets following the function name "make_plot". Getting this wrong will result in a ValueError. Forgetting to wrap the function reference in a string will result in a NameError.

Now in the other direction, perhaps we have a numeric input passing integer values from the user to the server. We’ll give the slider widget the id="int_slider". Now when we want to use the value of this slider on the server-side, we use a different syntax:

def print_selection():
    n = int_slider()
    return f"You chose {n}!"

Notice this time, we include brackets after our call to the widget id: n = int_slider(). Weird, right? Getting this wrong may result in unexpected behaviours. Keep an eye out for this. Also, wrapping server id references in speech marks results in unexpected behaviours, but not necessarily errors.

If I haven’t lost you yet, well done! Debugging applications can be a very frustrating process - part intuition earned from hours of Shiny debugging, part Stack Overflow and part coping mechanisms. I’ll cover some of those in the Tips section.

Exercise

Try modifying the app provided in the previous examples to repeat the greeting a number of times specified by the user.

  • You will need to include a UI input that will collect numbers from the user.
  • Update the greeting() function to return multiples of the greeting string.
  • Explore the other text output functions to avoid the message being truncated.
  • If you’re stuck, click on “Show the code” to see a solution.

Show the code
from shiny import ui, App, render

app_ui = ui.page_fluid(
    ui.input_text(
        id="name_entry",
        label="Please enter your name",
        placeholder="Your name here"
        ),
    # 1 update the UI with a way of taking numbers from the user. Here I use a
    # slider, but a numeric input or even radio buttons would also work.
    ui.input_slider(
        id="n_greetings",
        label="number of greetings", value=1, min=1, max=10, step=1),

    #2 Change to output_text instead of output_text_verbatim, which uses strict
    # rules for word wrapping and would hide most of a long greeting.
    ui.output_text("greeting"),
    ui.tags.br()
)

def server(input, output, session):
    @output
    @render.text
    def greeting():
        """
        This function will take the output of the ui.input_text() element,
        format the string in a polite sentence and format it as an HTML
        output for the ui to show.
        """
        # multiply the greeting string by the number of times the user asked
        return f"Hi {input.name_entry()}, welcome to Python Shiny." * input.n_greetings()

app = App(app_ui, server)

Ready, Steady, Go!

One final adjustment to this app. When you’re typing a name into the text input field, there’s a bit of a race going on. Can you type faster than the server can render the text? This may not be what you want. In fact, you may require a bunch of selections to be made prior to calculating anything in the server. We can use methods to interrupt and isolate elements of the server. In effect, we can tell any of our server functions to hang fire until a certain condition is met. In this example, we’ll try out perhaps the simplest way of achieving this, enter the ui.input_action_button().

Show the code
#1 Import the reactive module from Shiny
from shiny import ui, render, App, reactive

app_ui = ui.page_fluid(
    ui.input_text(
        id="name_entry",
        label="Please enter your name",
        placeholder="Your name here"
        ),
    #2 add an action button to the ui, much like we did with the text input
    ui.input_action_button(id="go_button", label="Click to run..."),
    # add some visual separation to the text output
    ui.tags.br(),
    ui.tags.br(),

    ui.output_text_verbatim("greeting"),
)

def server(input, output, session):
    #3 add an additional mark below the others
    @output
    @render.text
    @reactive.event(input.go_button)
    def greeting():
        return f"Hi {input.name_entry()}, welcome to Python Shiny."

# This is a Shiny.App object. It must be named `app`.
app = App(app_ui, server)

You should now see that an input button has appeared and the sentence won’t get printed out until you press it.

Also notice that the inconsistency in how to refer to functions on the other side of the ui:server divide rears its head once more. All in the server, when we want to use the values returned by the text input, we use the syntax input.name_entry(). When we want to use the action button in the reactive decorator, we have to use input.go_button - no parenthesis! The docs describe this as when you need to access the returned value versus when you need to call the function. This does make sense but can introduce some cognitive conflict while you are working with Shiny. I hope by version 1.0 the development team can find a way to simplify things.

I also included some visual separation between elements in the ui by using ui.tags.br(). If you know a little HTML, you may get excited at that. You can access all the typical HTML tags in this way:

Show the code
from shiny import ui, App, render

app_ui = ui.page_fluid(
    ui.tags.h1("A level 1 heading"),
    ui.tags.h2("A level 2 heading"),
    ui.tags.br(),
    ui.tags.p("Some text ", ui.tags.strong("in bold..."))

)

def server(input, output, session):
    return None

app = App(app_ui, server)

Exercise

Do you know enough markdown syntax to convert the ui below from HTML tags into markdown? This will greatly simplify the code. You will need to use ui.markdown("""some multiline markdown""") to achieve that.

  • You can use this markdown cheatsheet to help.
  • If you’re stuck, click on “Show the code” to see an example solution.

Show the code
#1 Import the reactive module from Shiny
from shiny import ui, render, App, reactive

app_ui = ui.page_fluid(

    ui.markdown(
        """
        # Hello Python Shiny!
        This **simple application** will print you a greeting.

        1. Enter your name
        2. Click run

        Please visit [some website](https://datasciencecampus.github.io/)
        for more information
        ***
        """),
    
    ui.input_text(
        id="name_entry",
        label="Please enter your name",
        placeholder="Your name here"
        ),
    #2 add an action button to the ui, much like we did with the text input
    ui.input_action_button(id="go_button", label="Click to run..."),
    # add some visual separation to the text output
    ui.tags.br(),
    ui.tags.br(),

    ui.output_text_verbatim("greeting"),
)

def server(input, output, session):
    #3 add an additional mark below the others
    @output
    @render.text
    @reactive.event(input.go_button)
    def greeting():
        return f"Hi {input.name_entry()}, welcome to Python Shiny."

# This is a Shiny.App object. It must be named `app`.
app = App(app_ui, server)

So there you have it. A very basic application that can be used to print out some simple messages to your user. This is of course a trivial app in order to keep things basic for the purposes of this blog. If you’d like to investigate what’s possible with Shiny, I’d suggest taking a peek through the Posit docs examples and the Python Shiny gallery. In the next section I’ll go over some tips that may help with common pitfalls I’ve encountered while working in Shiny.

Tips.

Shiny for Python VSCode Extension.

The Shiny for Python extension is a convenient way to launch Python Shiny apps. It adds a ‘Run Shiny App’ button to the VS Code interface, allowing for options to serve the app locally within a dedicated VS Code viewer window, or alternatively launch the app directly within your default web browser.

In order to run your application with this extension, you must ensure your app file is saved as app.py, otherwise the run button will not recognise that the currently selected document is a Shiny app.

Header Accessibility Adjustment.

A big shoutout to Utah State University for making their fantastic suite of web accessibility-checking tools open source. These tools make checking the accessibility of your web products much easier. Simply visit the Web Accessibility Evaluation Tool (WAVE) and enter a url under “Web page address:” and press return. The site will launch a helpful overlay on top of your specified url, highlighting accessibility alerts, warnings and features. There is also a sidebar helpfully explaining why the various alerts are important and what can be done to resolve them.

Unless you have managed to host a Shiny application on a service such as shinyapps.io, unfortunately you won’t have a url to pass to WAVE. Working locally on your machine, your locally hosted app interface will launch with a url like: http://localhost:… There is another way to use WAVE to check localhost sites. Using the WAVE browser extensions will allow you to launch the WAVE tool within any of your browser windows. This would allow you to run these checks locally on your machine while also ensuring that your app looks good on Chrome, Firefox or Edge. When checking basic Python Shiny apps for accessibility, one of the common accessibility errors you will encounter will be:

Language missing or invalid!

The language of the document is not identified or a lang attribute value is invalid.

This means a screen reader may not detect the language of the website content and may not work properly. The ideal scenario would be that Shiny would ask you to specify the language of the application that you are working in at the point where you create the application. It’s a straightforward fix though. We can include some tags at the top of our ui that will update the web page content with the required information:

Show the code
from shiny import ui, render, App, reactive

app_ui = ui.page_fluid(
    # Add a header and specify the language and title.
    ui.tags.header(ui.tags.html(lang="en"), ui.tags.title("A Really Basic App")),

    ui.input_text(
        id="name_entry",
        label="Please enter your name",
        placeholder="Your name here"
        ),
    ui.input_action_button(id="go_button", label="Click to run..."),
    ui.tags.br(),
    ui.tags.br(),

    ui.output_text_verbatim("greeting"),
)

def server(input, output, session):
    @output
    @render.text
    @reactive.event(input.go_button)
    def greeting():
        return f"Hi {input.name_entry()}, welcome to Python Shiny."

app = App(app_ui, server)

Missing Commas.

One of the more frustrating aspects of debugging Shiny applications, particularly as the application grows, is that a single misplaced comma can completely break your application. The thing to look out for here is when you run your app and you get the unexpected SyntaxError: invalid syntax error. I recall this being a real headache when I was learning RShiny, so much so that I would leave a little comment after each comma reminding me of which element of the code was being closed within the preceding bracket.

The Python errors are really helpful. They not only point to a problem with the syntax, but they identify the line number in the app where the first instance of this issue was encountered.

Debugging Errors.

At times it can be unclear why errors are being raised and it becomes important to investigate intermediate values that are being calculated in the server. Your usual python debugging tools may not work with Python Shiny. Shiny is event-driven and the reactive flow and order of the object definitions in your scripts are treated a bit differently. Without a tool like R’s Reactlog package, this is currently quite tricky to do in Python. The main coping mechanism available at the moment is to include some text outputs in the ui, paired with render text functions in the server. You can go ahead and use these elements to helpfully print out intermediate values such as DataFrame column names, data types etc and then comment them out when they’re no longer needed. Examining these intermediate values from within your ui is often the way to go when you can’t understand the root cause of your problem.

Make a Reprex!

One more approach to consider is to try to isolate your problem.

Sometimes it can be hard to build a mental picture of all the moving elements under the hood in your server, and how they may be unexpectedly interacting with each other. The problem you’re encountering may be due to these complex interactions in your server. Simply commenting out the code not directly related to the issue you are experiencing helps to triangulate the source of the issue within your reactive model. This is also the first step towards producing a reprex - a reproducible example. These should be as minimal as possible and are very helpful to start getting to the root of a programming problem.

Specifying Working Directory.

This is more of a consideration for deployment to a service such as shinyapps.io than something you tend to encounter while learning the ropes. And you likely won’t encounter this issue if your app.py file is located in the top level of your project folder (also known as the project root). If your app is not in the root of the project folder, you may wish to include this snippet of code before you define your Shiny ui:

os.chdir(os.path.dirname(os.path.realpath(__file__)))

This ensures that the working directory is set to that of the app.py file when it runs. If you encounter pandas errors complaining about FileExistsError when you deploy your app to shinyapps.io but not when you locally run your application, this may be the fix you need. Also something to consider if your app is styled correctly locally but not when you deploy. Potentially a relative path to a dedicated stylesheet has broken.

One more thing on deploying your app - if you do intend to host your app for others to use, I cannot emphasise this enough:

Deploy Your App Early!

Deployment of applications is not a straight forward, half an hour job. There are often inconsistencies to iron out between the environment you developed the app in and the server that will be running the remote app for your users. Deploy your app early when it is basic and you can catch these inconsistencies as you go. Or don’t and ignore the pile of technical debt your project is accruing. These are your choices.

In Review.

We have written a very basic application that is not much use beyond a basic tutorial. Although we have successfully demonstrated how to have the ui and server elements of a Shiny application talk to one another. We’ve captured dynamic inputs provided by the user and presented them back within the interface. And we have been able to pause the server execution until the user asks for a response. That’s not a bad start at all. But so much more than this can be achieved in Python Shiny. A good place to go for inspiration is the Posit Example Gallery. And if you’d like to understand a little more about how Python Shiny fits into the Python dashboarding toolkit, please check out my other blog on The Current State of Python Shiny.

Happy dashboarding!