
Making a (Python) Web App is easy! - posit::conf(2023)
Presented by Marcos Huerta Making Python Web apps using Dash, Streamlit, and Shiny for Python This talk describes how to make distribution-free prediction intervals for regression models via the tidymodels framework. By creating and deploying an interactive web application you can better share your data, code, and ideas easily with a broad audience. I plan to talk about several Python web application frameworks, and how you can use them to turn a class, function, or data set visualization into an interactive web page to share with the world. I plan to discuss building simple web applications with Plotly Dash, Streamlit, and Shiny for Python. Materials: - Comprehensive talk notes here: https://marcoshuerta.com/posts/positconf2023/ - https://www.tidymodels.org/learn/models/conformal-regression/ - https://probably.tidymodels.org/reference/index.html#regression-predictions Corrections: In my live remarks, I said a Dash callback can have only one output: that is not correct, a Dash callback can update multiple outputs. I was trying to say that a Dash output can only be updated by one callback, but even that is no longer true as of Dash 2.9. https://dash.plotly.com/duplicate-callback-outputs"" Presented at Posit Conference, between Sept 19-20 2023, Learn more at posit.co/conference. -------------------------- Talk Track: The future is Shiny. Session Code: TALK-1086
image: thumbnail.jpg
Transcript#
This transcript was generated automatically and may contain errors.
Hi, everybody. Thanks for coming. My name is Marcos. I'll be talking about Python Web Applications and how making them is easy. My background is in astronomy, but now I work as a data science manager at CarMax where I do pricing algorithms, but I will not be talking about space or car prices today, but making Python Web Apps. And by making a Python Web Application and learning how to do it, you can better share your ideas, your code, and your projects with a broad audience.
So it's good to share your ideas. It's good to share your stuff. My focus is really kind of on maybe open research or a side project. I'm not really going to focus on kind of deploying a big dashboard for your company or a big production app, but just kind of the basics of how these Web Application frameworks work and how you can use them to kind of, you know, share your ideas with the world. And I do think that once you have learned these tools or any of these tools, you can definitely apply it at your day job. That's how it happened with me. I kind of taught myself Python Dash just for my own edification, and then I did end up applying it, end up using it at my job after I kind of taught it to myself for fun.
So this is my hierarchy of sharing. It's not really a hierarchy, but, you know, you have some idea. You found something cool. You just do nothing. You just do nothing. But maybe you, like, cracked a lottery or something. You don't want to tell people, so that's fine. You don't always want to tell everyone everything. Then, you know, maybe you've got, like, a code snippet. You know, maybe you have some cool function that does some interesting data production. You could put that in a gist or a short blog post. Next thing, you can imagine, like, a GitHub repository where you, you know, people can clone it and play around with the Jupyter Notebook or see your datasets and such. Above that, again, this is more for code. You could make an installable package and put it up on PyPi.
And then kind of above that, not really above it, now we're kind of getting into some, not necessarily a hierarchy anymore, but for certain applications, a static web app is a perfectly good way to share your ideas. So we just heard a bunch of talks about Quarto in this room, so a great way to build maybe some interactive plots, but also kind of describe some interesting stuff that you found. But then somewhere at the top of this hierarchy is the interactive web app where you can really kind of go back and forth. The users can interact with your application.
A web link has no friction. So one of the things I'm going to show is this semantic emoji finder web app that I built, and it started as a function that you would just return a pandas data frame, but then I built it out into a web app, and when I shared this link with anyone, they can all kind of experience it without having to install Python, without having to be a data scientist. They can just play with it, and it kind of broadens the audience beyond maybe just other data scientists or other coders, et cetera.
So you're thinking, well, this sounds great, but this sounds hard. I don't know JavaScript. I don't know HTML. Well, a web application framework like the ones I'm going to be talking about today will do most of this hard work for you. They handle the server stuff. They handle the buttons. They handle the interactivity, and you just get to write Python, and then the app kind of springs to life for you. You don't have to write other languages. You don't need to know JavaScript, CSS, et cetera. You can just write Python. A little CSS won't hurt. We'll get to that. But the point is data scientists aren't web developers, so you can focus on writing Python and what presumably you are comfortable doing.
Well, a web application framework like the ones I'm going to be talking about today will do most of this hard work for you. They handle the server stuff. They handle the buttons. They handle the interactivity, and you just get to write Python, and then the app kind of springs to life for you.
So these are the ones I'm going to be talking about. Obviously, this whole session is about Shiny, so I will talk about Shiny for Python at the end, but I'll also start off with a stream in Dash, and then I'll kind of give an overview of some non-enterprise deployment options if you want to put these apps out into the world for people to play with.
So this is the app we're going to be building. I needed a QR code for the end of this talk because, as I think people have seen, it's very popular to put QR codes at the end of a talk to link out to stuff, and I'll do that too. But when I went to make one on the web, I got a lot of this. I got a lot of please log in, please sign up to download the QR code, which I didn't really like. So I was like, well, I'll make one on my own, my own computer. But then I thought, wait, this is a great app. Instead of doing it in Python on my computer, I can make it into a little web app. So that's what we're going to do. We're going to make a demo app. I made a demo app of QR Code Generator. We'll do that three times in the three frameworks, and then I will show but not show code for that semantic emoji search app that I talked about, just so you can see how I implemented it three times.
Streamlit
All right, so Streamlit is the first one we'll talk about. I think they were bought by Snowflake sometime in the last year plus. They will tell you that you have a Python script, you sprinkle in some Streamlit, and then suddenly, lo and behold, you have a web app. Simple things are simple in Streamlit, is how I like to describe it. So QR Code app, very simple. It's kind of like in Streamlit's wheelhouse, very easy to implement. So you're going to see that it's literally six lines of code to do this in Streamlit.
Simple thing, simple. I've kind of diagraphed it out here at the top. We've got the imports. Then we have an st.txt input. That's going to be our variable. That's going to be set to the content variable, and that's just going to be the little form on the page where you can type in a string. Then we're going to check to see if that's truthy. Then we're going to use Segno, which is a very nice library to make QR codes. It has no dependency, so that's why I picked it. That's going to kind of create the QR code object, and then create the image data that we need. We're going to pump that out of the app with st.image that will write the image to the screen.
You can see a screenshot there, what it looks like. We'll go ahead and run it. Obviously, simple app, simple application, the perfect kind of use case for Streamlit. There you go. Boom. We have a little QR code app.
The high-level issue with Streamlit is it runs the entire script every time. For more complicated apps and more complicated interactivity, that could become a problem. You might have two outputs. You might have a very expensive function, like a model run or something. You have to cache that. Those are considerations. I don't typically run into these, the kind of apps that I build with Streamlit, but it is potentially a concern.
The issue I ran into when I went to show you the emoji search app is I just wasn't really satisfied with the look. It works. I'm searching for launch. I'm getting a rocket, but the icons are kind of small. I want this to be a table, but it's really two parallel columns because I couldn't get the kind of tables I wanted out. It works. This is actually the first version of this app that I wrote, but it really wasn't satisfying. It wasn't really scratching my itch, mostly because of these layout issues, a little bit of a lack of control over how you lay it out.
Plotly Dash
Let's talk about Plotly Dash. Like I said, it's from Plotly. You may use their graphing library and their plots, which are great. It's based on Flask and React. If you are a web developer, you may have very strong opinions about React. I'm not, and I don't, but it is based on React and Flask. One interesting thing about it, there's no what's called server state. Every time you click a button in Dash, it's sending everything from your browser to the server and back, which kind of makes it a little chatty sometimes if you're sending a lot of stuff back and forth.
Both Dash and Shiny, you explicitly define a layout. You saw in Streamlit, there was no code saying what the layout's going to look like. Both Shiny and Dash, you have to explicitly put a do layout. That might be some inputs, like a text field, which is what we're going to have for the QR code app. It could be a little pop-up menu. It could be a date picker. There's all kinds of different potential inputs you could enter. Then we're going to have some kind of functions in Dash. These are called callbacks. Then we're going to have outputs. For the QR code app, it's just going to be an image, but you could imagine, of course, for a more data sciency app, a graph, a table, maybe multiple things like that.
One interesting thing about Dash is that it does not distinguish between inputs and outputs. When you see the layout on the next screen, any of those things that you will see could be an input or an output because Dash can read and write to both, which is kind of neat.
Here's the QR code app now implemented in Dash. It's just showing the layout. I kind of skipped the imports so I could fit it all on the screen. We're going to go through these one line by line. It's definitely more verbose than Streamlit, right? Streamlit was six lines of code. Here, just my layout is 12 lines or something. HTML.h1, and I said you wouldn't need to know CSS. That's CSS right there when it says style, text align center. That's just making our headline. Then we've got a little Dash bootstrap component. If you do make Dash apps, I really recommend using the Dash bootstrap components. They're really useful. It makes things a little prettier. I'm using an input group to kind of group the button and the text field together. Note that they both have IDs. We have this keyword argument ID equals content and ID equals go. We'll need those later when we want to make this app do something. We have an input and a button. Then we're going to have our little image div to kind of take the data when the app does something. Again, notice there's an ID. ID equals output image. Then there's a little CSS to style it, to center it. That's where the stuff's going to go.
How do we make it do something? That's just a layout. It's not doing anything. Let's make it do something. That's with a Dash callback. We have this decorator in front of the callback function. This is what kind of links all of those elements to the function so it can actually do something to the app, to the application. The output, we have every Dash callback can only have one output, which is somewhat of a limitation. It might not be true anymore, but most of them have one output. That's obviously going to be our image, and we're going to put it into the SRC source field of the image output, which is where we want our data to go. Then we want the content, of course, of the text field, and we want the go button to make this thing go. You notice that the second one is state, not input. That is telling Dash, don't react when the content value changes. When you start typing into that field, don't fire this function off. Only fire this function off when input is clicked, so we get the number of clicks of that go button.
Now, the number of arguments in this callback function has to equal the number of inputs, so we have one for content. That'll be our content variable, our string. Then the number of clicks is just an integer that goes up. We don't really care what that number is, so we're going to just throw it away and not worry about it and assign it to the underscore variable. Then this code should look familiar, right? Same code we saw in the Streamlit app. Make the QR code, return the data. Then this is kind of boilerplate to make the app run locally on your computer.
This is what the app looks like. I think it looks a little nicer. We got a nice little bootstrap button there. We got a nice little centered everything. I think it looks nice. This has been deployed. I have a version of this. It has a little added a download button, so this is deployed on my website. There'll be a link to that later.
Let's look at the complicated app, write the emoji search. Again, no code, just looking at it. This is the canonical version of this app. This is the version that I like. This is the version that I send people. I've got a nice font. I've got these settings, these hideable settings where you can surface and priorities of search results for skin color or gender. There's actually a font adjustment scale if you want more results or you want more bigger font. I love this version. This is the version I send to people. It works very well.
Shiny for Python
All right. Now, this is a Shiny session, so probably a lot of people here know more about Shiny than I do. Let's go ahead and look at Shiny for Python, how I did this in that app. Obviously, Shiny for Python was announced only a year ago, so it's very young. I did start in Shiny. I built our Shiny apps way back when, before I became mostly a Python person. I was very excited when this was announced. It was fun to play with it and try and re-implement some of the things that I built and other frameworks in Shiny for Python.
Much like Dash, we're going to explicitly lay out the UI. Of course, instead of using Dash functions, we're going to use Shiny functions. We're going to make a little page wrapper. Instead of a HTML.h1, we've got a UI.panel title, but the same idea. We're going to make a title. I'm taking advantage of this cool sidebar layout that's built into Shiny. We're going to have the text field and the button in a little small panel, and then the image in a right panel. You notice that there's also an id keyword argument to identify the elements. We have id equals text and id equals go to label the input text and the go button. Then we're going to have a output UI with the id qr code to hold the image. Notice that unlike Dash, the inputs and outputs are explicit here. We have the inputs all start with input and the outputs start with output, because unlike Dash, they're separated into two classes.
Unlike a callback, Shiny has these Shiny server functions. Instead of one long decorator, we have two short decorators. Output tells the Shiny server this is going to make output, and then render UI says the kind of element it's creating. It could just be render.text, for example, if you were just outputting text. Again, unlike Dash, where we have to tell it in the callback what's an input, what's a state, Shiny just figures it out based on what's in the function. Input.go is the variable. Is this the button? It's a number. Then input.text will be the string we want to convert. I only want it to fire off when I hit input.go. I just have this throwaway function here to make sure that this function will fire off with the go button. Then I isolate the input.text field, so when input.text changes, this function doesn't keep running over and over again every time you type a new letter. There's another way to do this with a different decorator, but I implemented it this way. Anyway, the rest of this looks familiar. This is the same conditional, the same stuff. It looks a little different, the syntax, but we're making the QR code and we're sending it out to the app. Then this is more boilerplate, just to create the app in the script.
Here is the Shiny QR code implemented once again, now in Shiny. Looks pretty nice. I like the sidebar layout. I've implemented a version of this as well and put it on my Shiny server. If you want to make QR codes with this app, it's out there and I will send a link, show a link at the end.
All right, so here is semantic emoji search with Shiny. I did not try to implement the settings for various reasons that I can get into, but I think it looks nice. This thing at the bottom is a bootstrap table. I built a little function that I linked to there that is turning a Pandas DataFrame into a bootstrap table. It is also passing in a special function to customize that right column, because obviously that right column is not just a single text field. It's got a lot of stuff going on in it. I made that customizable. That's a little beta, but it's out there if people want to play with it.
Deployment options
All right, let's talk about deployment. This is, again, non-enterprise deployment. Obviously, there are a lot of knowledgeable people here at this conference to talk about enterprise deployment. There's Posit Connect, which can actually deploy all three of these things, but let's just talk about you as a hobbyist wanting to deploy things. I'm probably preaching to the choir here, but Shiny has a lot of nice ways to deploy small apps to Shiny server, which I have installed on my web server. It's free. It's open source. It does require you to have some kind of cloud web server. I have one myself, but you do have to understand that, so not necessarily super easy if you're not familiar with setting up a web server. ShinyApps.io, which I hadn't used until this week, very easy to use, very easy to deploy a small app on ShinyApps.io. Obviously, if your app is really big or really memory intensive, you might need to use a paid tier. Then ShinyLive.io, also a pretty new idea to run some of these simpler apps in a browser, which is another way to do it.
Streamlit has something very similar to ShinyApps.io called Streamlit Community Cloud. You just point it to a GitHub repository, and it brings up a little Streamlit app, but also limitations on how much RAM that has and stuff, so you can't run a really big app that way, and limited by GitHub's file size limits. Then ironically, I've deployed probably seven to eight dash apps on my own website, but I cannot call it particularly easy. I did write a whole blog post about how I do it. It is a standard, what they call a WSGI Python application, but it is not particularly trivial to deploy. You can also use things like Heroku or Python Anywhere, but happy to talk more about deployment things if you want.
I would encourage you to not really worry about deployment at first, just kind of play around with one of these frameworks, play with all of them, run them locally. I think when I first started using these at my day job, I was just running it locally, and that actually got a lot of value out of that. But you will get emotionally involved if you start making these apps. You will want it to run. You want it to look pretty. You want it to look a certain way, and then you want to share it with the world. You will eventually probably want to deploy it, but there are ways to do that.
You will get emotionally involved if you start making these apps. You will want it to run. You want it to look pretty. You want it to look a certain way, and then you want to share it with the world.
I think that's the end. I want to thank all of you. I want to thank my managers at CarMax for helping me come out here, and I'm happy to take a few questions. And that is the QR code to my talk notes. It's like a little blog post I wrote. It has links to all the official documentation. It has links to all the demo repositories. It has links to the live apps that I've deployed, and I'm on the Discord if people want to tag me in the Python channel or anywhere else.
Q&A
I'm happy to take a few questions if we have any. We have some questions for Marcos. The first question is for app layouts in Dash. Is there a way to refactor the layout into a class or functions to make it more readable and modular, I guess? That's a good question. A lot of people, when Dash apps get more complicated, they break it out into a separate file and import it into the main file so it doesn't pollute your one big file. You can also break it out into sub, not function, but sub variables. That's what I usually do when I write a Dash layout. I'll have variable equals this is the output page, and I'll make that a variable, and this is the input section. I'll make that a variable. That helps clean it up a little bit so that it's a little easier to follow instead of one big block of nested Python lists and stuff.
Next question is, what's the best way for a Python developer to learn enough CSS to be dangerous? That is a great question. I tend to steal other people's CSS, is usually my advice, so that every one of these frameworks lets you sprinkle in CSS a little differently. Dash has these Python dictionaries that match the keywords. I think Shiny, you put the whole CSS string in there. CSS is always an adventure. I think I've tried to get one of these things centered. It took me a lot longer than it should have. I'm always Googling CSS. It is always a challenge. Like I said, you get emotionally involved. You're like, I want this to be centered, and then you spend a lot of time trying to figure out the CSS to get it centered.
