Resources

An Interview with Winston Chang: Building a Wordle App with Shiny for Python || RStudio

Shiny makes it easy to build interactive web applications with the power of Python’s data and scientific stack. Learn more about Shiny for Python: https://shiny.rstudio.com/py/ Check out our interactive Shiny for Python examples: https://shinylive.io/py/examples/ Content: Winston Chang (@winston_chang) + Jesse Mostipak (@kierisi) Producer: Jesse Mostipak (@kierisi) Editing and Motion Design: Tony Pelleriti (@TonyPelleriti)

image: thumbnail.jpg

Transcript#

This transcript was generated automatically and may contain errors.

All right, so this is the state of the Shiny website at the time of this recording, so it might be different by the time people actually see this. But if we just start from this landing page for Shiny for Python, then you can click on examples and there's a whole bunch of examples here now, and you can select them from the sidebar. So I'm just going to go down. Wordle is all the way down here in this advanced section, and then here's our Wordle app.

What's your Wordle starter, Mark? My Wordle starter? It's Arise. Can we play Wordle? I haven't played Wordle since... All right. Let me make this a little bit bigger here.

Are you still playing Wordle every day? I'm not. You know, I was really hardcore for a while, and then I just stopped and forgot one day, and then I forgot the next day, and I just haven't really done very much of it since then.

So my starter is Arise. I'm just going to click on my keyboard. Did you just hit enter to make it do that too? Yes, I did.

And actually, I should point out that that keyboard interaction, that is some custom JavaScript that's added to this app, because normally, you know, for a normal webpage, if you just type in it, it's not going to send the key presses to necessarily where you want to go. So there's a little bit of JavaScript, which I'll walk through that does that. It's pretty simple.

So what's your strategy? So I'm going to keep A-I-S in my next guess, or are you like, nope, I'm going to go for full coverage. I'm going to pick something that excludes those. You know, unless I think I can get it, the word in the next guess, I usually use another word that will cover the rest of the vowels. So my next word that I usually use is pouty.

This is amazing. You have a whole strategy. So now I know there's no other vowels and yeah, there's T-A-I-N-S and so far they've all been in the wrong spot. So my guess would be that the S is last. That's common. And well, that would be if S is last, it would almost be like blank A-I-T-S, which is all except for the T and the I can't be in those locations. Keep coming up with words that have the letter R, which is not helpful.

And stain? Stain? S-T-A-I-N-E? That's pretty good. Hey! Nice work, Jesse. Teamwork! Nice. That's pretty good. Three.

Awesome. And then it's got like a nice little, you got a little pop up. Yeah, you can take this and you can copy and paste it into Twitter and then just really confuse the heck out of everyone there.

Building the Wordle app from scratch

Now, didn't you build this so that you could play in as many guesses as you wanted? Yes, that was, yeah, sometimes I get a little lazy and I just want to guess and not have to think too hard about it and worry about failing. So yeah. So this one doesn't, it doesn't stop. You can just keep guessing as many times as you want.

I was, let's start with like the very minimal thing that, you know, like the smallest, simplest possible thing that we can build and try to evolve that into, into this game here. So that started out with just, you know, having a simple text input and letting people type in some stuff and hit enter. And then from there it's like, okay, now let's try to limit it. Let's limit that to five characters. Let's actually add a word list so that, you know, they're guessing a particular word. Let's save all their previous guesses and then display them.

For the Wordle app, I could sort of, you know, I could sort of see a path from like this is very simple. Let's start with just text input and let's try to turn it into, you know, this full, this full fledged game here. And that's something that I guess you get from experience. Like you can see like, like, okay, I think it's possible to get from, you know, from point A to point B, but sometimes it's not, you can't just start with something really, really simple and, and evolve it into something that looks nice and sophisticated like this.

So sometimes you do have to sit, well, for me at least, sometimes I have to sit down and just figure out like, okay, how am I, what are the going to be the big parts of this and how are they going to fit together?

Learning Python and the tooling challenges

How much Python did you know when you were doing this, like when you started this? So when I started the Wordle app, I think I've had, you know, I've been programming, I mean, spending most of my time in Python, well, and also in JavaScript for the last, just about the last year or so. But before that I barely used Python at all.

So yeah, but I think by the time, when I started this app though, when did I do this? This was about four months ago, I think. I think it was longer. Was it? I feel like this was January, February. Oh, you might be right. Yeah. This might've been one of the first things. We were like, hey guys, look what I made. We were all like, what?

I remember it to be like, at least the way that I remember it, the story that I'm telling myself is that you were like, had a couple hours, thought I'd do something fun. And now Wordle, like with Python in the browser and you can do unlimited guesses. Yeah. Well, I mean, it helped that there was, you know, we did, we already did the R one first.

So, I mean, just for context, how long have you been writing R code? I mean, probably the last, I would say maybe 11 years doing that. Yeah. So that's, that's, that's a lot of time to be working in R.

Yeah. So, I mean, there are, there's a lot of, there's a lot of nice things about Python. Like I think it's like in terms of just like the language itself, it's, it's, you know, it's designed to be sort of, it's be easy to learn and it encourages sort of a clean coding style. There are some annoying limitations if you come from a functional programming language like R or JavaScript, you know, you, you can't, it doesn't really support anonymous functions. I mean, it does if they're, if they're like one statement or one expression, but you can't have a multi-line anonymous expression, which, which means that you ended up creating all these functions that, that you have to give names to. And then, and then you can, before you can pass them to another function.

And there's some, there's some really weird stuff and difficult stuff with the tooling for, for Python, like just not in Python itself, but like all the stuff around Python, like how do you install packages and, you know, and use them in a reliable way? How do you, how do you build a package? There's like a zillion ways to do things. And that is, that's, that's pretty difficult.

When I was first, you know, looking into becoming a data scientist, I was going to learn Python and it took me forever to get it installed. Sometimes I couldn't get it installed, but then it was like, you know, pip install, whatever. And no one told you where to put the command pip. And so I would put it in my browser. Yeah.

You were in Python and it launches Python two on your computer instead of Python three. And then none of the stuff that you installed with pip is there. And it's like, what, why isn't that working?

The online editor and shinylive

That stuff is not fun, but actually that's, that's one reason that I'm really excited about all this online editing stuff that we've built. You know, we have this editor here where you can just get started running Shiny. And all the stuff's already installed. You don't have to, you don't have to install Python. You don't have to install any packages. You don't, I mean, you don't even have to run this on like a normal computer. You can run this on an iPad where you wouldn't even be able to install Python normally.

So, and, and, you know, there's all, you get all this, all this assistance now, like you know, if I want to start from working in this UI here, so I can say like UI dot, you know, input underscore checkbox and, and then it's got all this auto-completion stuff. This is new actually. Yeah, this actually wasn't, this didn't exist in the last video that we recorded. I, this was just implemented in the last couple of weeks.

So, so yeah. And then, and you can hover over it and there's, that's a little bit finicky, but there's, it gives you the whole documentation right here. So that's, that has been super helpful. That's really helpful for coding in the browser. And it also has that support for that, for the Python standard library. So, you know, I can say from OS import path, and then I can say path dot, you know, base name, and then it, it, it gives you all these, these hints here to, to, to write the code.

So that will hopefully be a big help for, for people to get started. And again, and you don't have to install all this stuff in your computer and get that all set up. Getting that, you can get that stuff set up, like for example, with VS code, which I often use for coding Python. And it's, it's, it's really, really nice, but there's, you know, there's more stuff you have to set up than it's like, okay, you got to set up VS code. You got to install the Python extensions. You got to configure the Python extensions to give you that, the right sort of assistance and not, you know, not, and have it not flagged too many things as errors. So there's a lot of work that can, you know, a lot of manual work that would, that would be involved if you set it up on your own system. But if you don't want to do that, you can just come right in here and get working right away.

But if you don't want to do that, you can just come right in here and get working right away.

I feel like we're cheating. I feel like there's a catch. Like, you're going to be like, you can do all these cool things. Like there, right. There's no free lunch. I mean, this isn't a theorem or anything like that, but like, if I want to learn Shiny, this feels like such an approachable way to do it.

Yeah. You know, I mean, you eventually, you'll probably, if you're doing a lot of Shiny writing a lot of Shiny code, eventually you'll probably hit, hit some sort of roadblocks if you, if you just work this way. So, you know, in, in developing this editor, we've tried to remove as many of those as possible. So you can, you know, you can save these files to your disk. You can load them from disk, even though this is all running in your web browser.

But when it comes to deploying your application, you're going to have to get into the, into the command line world. And if you want to do stuff that, that you can't do in, in this version of Shiny, that's running completely in the browser with, with Python compiled to WebAssembly. So if you want to fetch data from a database and you don't want to share those credentials with the rest of the world, or you just, you want to work on a dataset where you need to, you need a really powerful computer to do some computation. This isn't going to really work for you. Like to, to send the code to the user's browser for them to execute it, that, that might not work. So then you have to run it, you know, then you have to run it the traditional way, which is with Python running on the server. And you're going to have to get your hands dirty with, you know, installing Python and configuring it and all that sort of stuff there.

Adding JavaScript and CSS to the app

This is all in Python, but you've also mentioned some CSS and some JavaScript. Okay. Yeah. Let's talk about this. So I'm going to actually switch over to another tab where I have it running the same thing running locally, because I've actually made some changes.

One of the things that I talked about was this JavaScript to deal with the key presses. So this keyboard here, these are all action buttons. And, you know, when you click on them, it sets a value that the server function can read. So this is, you know, obviously that's the button P there and I can hit backspace.

Now, in order to trigger these with the keyboard, I had to set up a little bit of extra stuff. And that's, it's relatively simple. So, uh, at least as this JavaScript code goes. So basically I set up in JavaScript, an array of all these letters, and then I add, uh, an event listener for a key down event, right? So this is just a JavaScript. So when the button is pressed, then it grabs, uh, it gets the element by ID. So like the elements that I created these buttons with IDs, you know, A, B, C, D, and so on. And then it triggers a click on them. And then there's special handling for the enter and back buttons. But, um, but yeah, that's basically it is. I just, I'm just basically like when somebody presses the keyboard, it triggers a click on the action button. And so the, and the rest of it is just sort of normal Shiny behavior.

Okay. So, um, and you know, if you're not familiar with JavaScript, but you still want to do this, it's really easy. You can just copy and paste this code. That's that's copying and pasting your code. What am I just putting it inside tags that script? Oh yeah. Yes. Right. So, uh, yes. So I have tags dot script here actually. And let me, let me explain that a little bit.

So tags is actually something that was, um, I imported from the HTML tools package. And that, that is something that has all of the HTML tag functions. So, you know, like div, span, um, script in this case, now you can do it that way, importing from HTML tools, or, um, another place that it's available is also via the UI object in Shiny. So I can say UI dot tags, actually need to delete all this. So let's start over. So you can say UI dot tags dot, and then these are, these are all of the, um, available HTML tags. So, so yeah. So the one that I want though is script.

So let's just put it in a script tag in HTML and then the browser knows that it's JavaScript. That's really cool. And then it's just all within your broader UI. Yep. That's, so that's just, that's just, you know, anywhere, um, in, in the UI for the app, um, you can put it, you can put it there and then the browser will just execute that JavaScript code.

That's really cool. Yeah. It's, it's, it's pretty easy to add the JavaScript. So when you're adding it, so this is all looks like it's in a doc string, but it's still. Yeah. Well, it's not exactly a doc string. It's, um, it's a triple quote string. Actually, you know what? I might, I could be getting my terminology wrong for Python. So for doc strings, you usually write them with, you know, triple quotes, but, uh, it just means that, you know, with the triple quotes, I can use multiple lines. Um, the string can span multiple lines.

But, but it's like, I mean, this would be a traditional way of like commenting something out, but you're saying because it's in the triple quotes, it's still reading this JavaScript. Yeah. Well, I mean, it's, yeah, it is. It's well, because it's in the triple quotes, it's just, it, it treats it just as a string, but I can still use, you know, I can, the string can span multiple lines and I can use quotes in it without, you know, making it confused that it's the end of the string. Um, unless I put three in a row to, to Shiny, this is just, this is just text. And it happens to be, I happen to be sticking it in the script tag.

Um, and so then, you know, actually if I do, I think I can do it in here. Um, yeah. So then Shiny is just like, okay, I'm going to just put the string in a script tag and that's going to be, that's going to be in the webpage like here, right here. Now there's actually a file in, in, in this app called style that CSS. And, um, what I'm doing here is I'm actually just reading in this files text, um, and returning it and that's, and then it's sticking it in, in the app as a string. So that's, you know, so I have, this is the styling, the CSS stuff for, for this Wordle app.

Um, and this is in this case, instead of, instead of putting it in line, like I did with the JavaScript, I, I read it in from the file. So that's another way of doing something that's similar. And actually the nice thing about that then is that this editor understands CSS. And so it gives me some syntax highlighting and, um, I think it'll tell me if they've got, well, maybe not. I thought it would tell me if there's problems with it, but, uh, it apparently does not, but it does have syntax highlighting. And, you know, if you use another editor, it might be smart enough to, to give you that sort of help.

So I could have, I could have put the JavaScript code in the same way. So, you know, create a new file with the JavaScript and then just insert it similar to this. There's multiple files in this app. And, um, one of them was that CSS file. And another, another one is this words.py file. So all the words are defined in this, this Python file, and you can just sort of import that the way you normally would from, uh, in a Python script. So you'd say import words.

Some of this JavaScript code here, um, like this stuff here, this was all touch interaction, like on your phone or iPad, cause that's, that's often how I play Wordle. And with the normal Wordle app or the real Wordle app, I should say, I find it a little annoying, like you, you touch the letter and it doesn't register until you let go of your, your, you raise your finger off the letter. So that, that leg kind of bothers me. Um, and so I added these other event listeners so that it, when you touch the letter, it immediately, uh, registers it. So you don't have to, you know, touch and then release. So that's, uh, that's what this stuff does here.

Reactive values and typed inputs

I'm really impatient for this UI interaction stuff. I love this is, I don't want to say it's like spite driven development, but it's like a little thing that you know how to improve. So you like make your own version of it. I love it.

So here's another thing that's unusual about this app that we haven't done in any of the other examples and it's to, uh, create actually a subclass of inputs. Um, where we specify the type of these various, uh, well, the various inputs. So, so this is, so this is what Shiny knows, like, Hey, when somebody presses the enter button, like what, what kind of value is going to come out of that? So we say, you know, class shiny inputs, and this is make, makes it a subclass of inputs and it is to clear all these things here, the enter button and the back button, those return, um, integer values, um, as does the new game button. And so that's, so, you know, like, Hey, this, this doesn't return a string or a Boolean, it returns, it returns a number, right?

So inside of the server app, first, I just say, Hey, take the input object and cast it to shiny inputs. Just treat it as shiny inputs. Okay. Then I can say, um, oops, I can say output, uh, render text and def my function here. Oh, wait. Now I need to get the name, call it def TXT. And if I say return, uh, input dot enter, it's going to say, Hey, there's something wrong here. Um, this error message is kind of long and cryptic, but the important part is function return type int is incompatible with the waitable string or none. So it's complaining about this, this returns a number, but this render text wants a function that returns a string.

So I can, you know, what I would have to do is convert that to a string. Now, it turns out that if you, you know, if you handle the number to render text, it will, it'll handle, it'll try to convert it to a string anyway, but there's other weird objects that you might give to it that, um, have the wrong shape. Like, uh, if you return, you know, a dictionary to it, or if you return to some sort of arbitrary weird Python object, it might not know how to handle it.

So, so having these, uh, declaring the types of these inputs and having it check them is, uh, is really helpful. Well, if you, if you are like really strict about things. So if I say like, Hey, I've got X, um, an X is a dictionary of, uh, or sorry, it's a list of strings. Um, I can't say, you know, assign this Boolean value to that list of strings. Well, for two reasons, one is that it's Boolean, the other one is that it's, you know, it's not a list. Uh, so it's not a list and it's not a string. Um, but like, maybe, maybe I said, Hey, X is a list of strings. I'll call it, I'll assign it, you know, this empty brackets first. Um, and then I could say X dot, um, append, you know, input dot heart. It's like, Hey, no, this is a Boolean. This is not a string. You've got, you've still got an error. So then I can say, Oh, you know what, that's right. I actually need to convert this to a string first.

Or maybe I want to say like, Hey, you know, like yes. If it's input dot heart else, no. So this is, this expression is going to turn either yes or no, depending on the value of input dot heart. But, um, but yeah, so having these types can help, you know, help you avoid bugs in your code by with, with, in combination with a smart editor like this, that will, you know, that will flag these type errors.

Oh yeah. Actually. Yeah. One thing that I wanted to talk about was this use of reactive values here, reactive dot value. So there's a bunch of these here. Um, one of them is that the target word, uh, one of them is like all of the guesses that, that the user has made in this round, whether the game has ended and what are the current guests, the current letters in the guests. So if, you know, if I type a R I, um, those letters are the, the letters in the current guests.

You don't see this for really basic Shiny apps. You don't see this reactive dot value stuff. Uh, let me pull up a really simple example here. So like, let's go to this basic app here. So this basic app, you just have an input, right? Input. And, and then that gets used by this output. So for an app like this, if I set the state of an input that will be consumed by the output and, um, and that's that. So there's no, there's no recording of history. Like what, like what was the previous state of an input that just doesn't, isn't part of this application. That's just totally, you know, that's just totally gone. Um, and that's, that's what happens if you just have inputs just that are used by outputs, but nothing to record the previous states.

So here I'm using reactive values to, um, record the history. So the history in this case was that I pressed A R I, and then, you know, I can fill that out S E if I hit enter, then, you know, then arise will be recorded in, uh, all guesses. So when you see these reactive values, that's often what you're, what it's used for is to, to help record some previous state. Okay. That's, that's important for, that's important for many, you know, more sophisticated applications.

Creating reactive effects in a loop

All right. So another unusual feature about this app is, so I'm, I'm creating reactive effects, um, that respond to each key press, but I'm doing that in a loop. So that is a little bit unusual. So let's see here. So, uh, there's, I created a list called keys. That's, um, where's that it's up here. So this is all the letters in the keyboard for all the ones that we want to use. And so I had all those, uh, all these action buttons here, right? This one's named Q W E R, you know, those are all individual action buttons. And I want it, uh, I want this application to, uh, update the current guest letters, uh, whenever one of those buttons is pressed.

Now, one way to do that is I could create a reactive effect for each, every single one, so, you know, I could do, oops, let's not put it there. I'll put it down here. I could say, um, uh, sorry, reactive dot effect, uh, reactive dot event. And this would listen to, you know, input dot, uh, Q. I think it's capitalized. And then I'd say def underscore. And then, and then in here, I'd say, okay, when the Q button is pressed, um, take the current guest letters and, oh, actually this is a little bit complicated. Oops. Current guest letters. Go get its function. Copy it. I'll just call this X, um, X dot append, you know, Q and then current guest letters dot set X. All right. So there's actually a few things going on here.

Um, I should, this is actually an important enough digression that I'm going to do it right now. Um, current guest letters is, it's a, it will return a list and you see that and you see that I'm copying this list, modifying it, and then setting a current guest letters to that new list instead of just taking that list and modifying it. So the reason for that is because this is a reactive value. And in order for it to sort of inform everyone that consumes, like all the things that use that reactive value, um, they need to, they need to know like, Hey, the value here has changed, uh, in order for them to invalidate and to know that they have to recompute.

Now, if I just changed, if I were to just take this and then append Q to it and not do this copying and setting stuff, uh, then I would modify that list, but I wouldn't, I wouldn't be informing everyone that it had been updated. So, um, and also it could confuse some of the other, uh, some other code. Like if I just modify that object in place. So that's actually one important difference between Python and R is that in R, most of these things, most objects are immutable. Um, and so you, you actually can't cause this sort of problem, but in Python, you have to be, you have to be sort of aware of, you know, what, what objects can be changed in place and how that can, um, that can cause problems with this reactive computation.

So that's actually one important difference between Python and R is that in R, most of these things, most objects are immutable. Um, and so you, you actually can't cause this sort of problem, but in Python, you have to be, you have to be sort of aware of, you know, what, what objects can be changed in place and how that can, um, that can cause problems with this reactive computation.

Now, I don't think that, you know, Shiny is unique in this, in this respect. Like I know if you're working with like pandas data frames, the same thing can also happen. But it is one more thing to be aware of, uh, compared to the R version of Shiny.

We would have to do this for like every single key on our keyboard. Right. Uh, I would have, I, so I did this for the letter Q, you know, I'd have to do for the letter W and, and E R T all of those, but that's obviously, you know, that's really tedious and it'll take up a bunch of space. And then if I found out that there's a bug in my code, I'd have to change all of them, which is a huge pain.

So instead of doing it this way, um, what I did was I created a, so there's a function make key listener, which creates this reactive effect that listens for a specific key. So it can take the name of the key. So, uh, you know, the first one would be Q and it's a, you know, it's listening for input bracket key. That's, this is actually equivalent to, so like I can do input dot Q here, or I can do input bracket Q those actually both work, those are equivalent. Um, you know, the dot syntax is a little bit more convenient if you're just typing it out for a single thing, but if you need to programmatically access an input value, you can, uh, you can do it this way in the, in the bracket.

So I'm saying, you know, listen for whatever key, you know, I was told to listen to, and then do all that same stuff that we just saw, except the key here is, you know, instead of hard coding Q it's, it's, it's comes in from this function call. Okay. So if I call this function, I can pass it the letter Q and the letter W and E and so on. Um, and that's what I'm doing here. I have a for loop that, uh, creates the key listener for all of those keys. And again, there's exceptions for entering back buttons because those are handed separately. Those are different behavior from the other keys.

Um, but yeah, but essentially it just, it calls us in a loop. Now you might be thinking like, Hey, can I just, instead of making this function here, can I put, just copy this code and just stick it right down here? Right. If I could say, Hey, let's, you know, forget putting it on this function. I'll just put it right there and properly indented. Um, so you, if you do this though, it's not going to work.

The, the problem is that it has to do with how it captures the value of key. Actually, let me, I'll just put it here first so that we can take a look. So what this is going to do, it's going to go, it's going to iterate all over all of the keys are got this inner for loop, this variable key will change every time, um, every time it goes through the loop. What ends up happening is it does create a separate reactive effect. For each one of those keys. But, um, this value of key is shared across all of them and it actually keeps changing. So let me, maybe it's best for, if I just demonstrate. So let me just run this app here. All right. So I've, I've changed it so that instead of calling this make key listener function, it's actually creating those effects in this loop here.

And if I press the button a, I hope this is actually going to work. It did something crazy. Wow. That is not what I expected. Oh, I did. Okay. This is actually the same problem, right? This is actually the problem I was talking about. It just, it just ended up somewhere where I didn't expect. So, right. So here's, let's see, all the letters are up here, right? So it just iterates over them in this order. So, you know, it goes from Q W all the way. And then the last one is back. So the letter, the value of key actually kept changing as it iterated over this.

And, um, the scope for this variable is like, it's scoped to the server function, this key variable, it's not saving the value of key for each time we called this loop. The, the, this, this variable key here is actually like at the top level of the server function. And it kept changing as we were going through the loop. Um, when we called it in this function here, um, instead of that key, keep constantly changing, you know, actually I can just, I can rename this to something a little bit. I'll just call this K to make it clearer. Um, like this letter K here, this variable K it's just scoped within this function.

So when I was saying, Hey, make, you know, make a key less narrow of key and then, um, it passed in the letter Q and then it created this reactive effect and reactive event. Um, and the variable K was, had the value of Q and it, that was captured within the scope of this, uh, within the scope of this closure. Okay. That's the, that's the terminology. So there's a separate closure scope for W and for E. And so that variable K is not sort of shared across all of those instances. So that's, that's, that's, that's what happened here as opposed to, you know, what we had, uh, in this messed up version where the variable value of key was shared across all of them. And so did something really bizarre.

So, um, I hope that made sense. I felt as soon as you said closure, I was like, I hear Jenny.

Wrapping up

Let Jenny be your guide. Yeah. Always literally. Yeah. So that's, um, that's most of the weird stuff that is in this application. Um, there are some CSS, I won't get too into depth about that. That's a whole, you know, that's nothing specific to Shiny about this. This is just like web development stuff, which, um, I, I'm also like, I'm okay with CSS, I'm not great with CSS. I wouldn't be the person to explain it. And a lot of this, a lot of the CSS to lay this out was just sort of like trial and error and me hacking away at it. Um, until I got something that looked decent.

I feel like, you know, not the CSS person myself, but I feel like that's the right way to do it. Uh, in, in sort of like the tree of complexity for your application, this is just sort of like a leaf node. It's just, okay, just make this work. It's not going to mess up anything else. All right. So yeah, that's Wordle.