











These days you can make your very own web app for free, in pure Python, with a minimal amount of code. The app we’re going to build is under 100 lines total. Not only that, the entire process, from coding and styling to hosting and embedding, can be surprisingly straightforward once you’ve seen an example. So let’s run through one now.

Say you have a gaming blog, and you want your home page to have an interactive web app that displays Twitch stream stats. More precisly, you want the user to be able to specify a video game and some number of streams, and then see a bar chart of the viewer numbers of those top streams, along with streamer names and stream titles (see the finished app at the end of the post).

First things first, let’s figure out how to get the stream data from Twitch.

Twitch API

If you wanted to you could scrape the info directly from the stream pages themselves. In general though it’s good to at least try out a site’s API if they have one before hacking together your own web scraper. And for our purposes, Twitch’s API will work just fine.

A little investigation of the developer documentation reveals that there are two Twitch APIs, the v5 API and the new API. We’ll be using the New Twitch API.

Follow the directions in the previous link to get your client ID, the key that authorizes you to interact with the API, then navigate to the reference guide’s “Get Streams” section.

If you want to get streams for a particular game, you’ll need to specify that game with the game_id query parameter. Note that game name is different from game_id , which is a number, so we’ll need to be able to convert game names entered by the user into game IDs for the API. This is described in the “Get Games” section.

Putting the two together, below is a sketch of our code for converting a game name to a game ID and then getting data on the top streams for that particular game.

import requests client_id = 'xxxxxxxxxxxxxxxxxxxx' headers = {'Client-ID': client_id} # get twitch id for Grand Theft Auto V url = 'https://api.twitch.tv/helix/games' payload = {'name': 'Grand Theft Auto V'} r = requests.get(url, headers=headers, params=payload).json() game_id = r['data'][0]['id'] print('Grand Theft Auto V ID: ' + game_id + '

') # get top 10 Grand Theft Auto V streams url = 'https://api.twitch.tv/helix/streams' payload = {'game_id': 32982, 'first': 10} r = requests.get(url, headers=headers, params=payload).json() for d in r['data']: print(d['user_name'], d['viewer_count'], d['title'], sep=' | ') 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import requests client_id = 'xxxxxxxxxxxxxxxxxxxx' headers = {'Client-ID': client_id} # get twitch id for Grand Theft Auto V url = 'https://api.twitch.tv/helix/games' payload = {'name': 'Grand Theft Auto V'} r = requests.get(url, headers=headers, params=payload).json() game_id = r['data'][0]['id'] print('Grand Theft Auto V ID: ' + game_id + '

') # get top 10 Grand Theft Auto V streams url = 'https://api.twitch.tv/helix/streams' payload = {'game_id': 32982, 'first': 10} r = requests.get(url, headers=headers, params=payload).json() for d in r['data']: print(d['user_name'], d['viewer_count'], d['title'], sep=' | ')

Grand Theft Auto V ID: 32982 pescocofino | 1366 | JOGANDO QUALQUER COISA Meta de SUBS: 125/200 !sub mande uma mensagem em: !loots Lord_Kebun | 678 | Mr.Chang Most Wanted | Twitter @LordKebun GameMixTreize | 506 | [FR] JE VENDS DU RÊVE SUR GTA V RP ! AladdinTV | 476 | [GVMP] Hakan Abi übernimmt die Stadt / Familiärer Stream Spaceboy | 325 | Melbert! | NoPixel | https://twitter.com/imaSpaceboy | !D20 TheChief1114 | 318 | back at it CivRyan | 276 | OCRP | Officer 247 | New Visuals and Sounds Selvek | 269 | Devin King - TFRP Men0rxD | 266 | [+18] [PT-BR] [UP-RP] Tamo com emotes quebrada, RPZIN naquele pique encosta #SemTiltar Bolshownaro | 248 | BOLSONARO DO GTA [+18] 1 2 3 4 5 6 7 8 9 10 11 12 Grand Theft Auto V ID: 32982 pescocofino | 1366 | JOGANDO QUALQUER COISA Meta de SUBS: 125/200 !sub mande uma mensagem em: !loots Lord_Kebun | 678 | Mr.Chang Most Wanted | Twitter @LordKebun GameMixTreize | 506 | [FR] JE VENDS DU RÊVE SUR GTA V RP ! AladdinTV | 476 | [GVMP] Hakan Abi übernimmt die Stadt / Familiärer Stream Spaceboy | 325 | Melbert! | NoPixel | https://twitter.com/imaSpaceboy | !D20 TheChief1114 | 318 | back at it CivRyan | 276 | OCRP | Officer 247 | New Visuals and Sounds Selvek | 269 | Devin King - TFRP Men0rxD | 266 | [+18] [PT-BR] [UP-RP] Tamo com emotes quebrada, RPZIN naquele pique encosta #SemTiltar Bolshownaro | 248 | BOLSONARO DO GTA [+18]

And just like that we get the streamer name, current viewer count, and stream title for the top 10 GTA V streams. This is essentially the core of our app, but now we want to display this data in a visually compelling way and make it easy to play around with the inputs. This brings us to building the app itself.

Dash: A Python Framework for Analytical Web Apps

Dash is a framework that allows us to build an app in pure Python, which, if you ask me, is a beautiful thing. They even have a nice user guide that makes learning it relatively painless.

Follow the directions for getting Dash installed on your machine, and read through the rest of their tutorial to get a feel for how Dash works.

Once that’s done, check out the code below, which is our finished Twitch web app. For more details on how the code works and how to run it locally on your machine, see the youtube video.

import dash from dash.dependencies import Input, Output, State import dash_html_components as html import dash_core_components as dcc import plotly.graph_objs as go import requests # twitch api info client_id = 'xxxxxxxxxxxxxxxxxxxx' headers = {'Client-ID': client_id} games_url = 'https://api.twitch.tv/helix/games' streams_url = 'https://api.twitch.tv/helix/streams' app = dash.Dash(__name__) app.layout = html.Div(style={'backgroundColor': '#4b367b', 'padding': 30}, children=[ html.H1( children='Top Twitch Streams', style={'textAlign': 'center'} ), html.Div( children='Select a game and number of top streams you wish to see', style={'textAlign': 'center', 'fontSize': 20} ), html.Label('Game', style={'fontSize': 20}), dcc.Input(id='game_name', value='Grand Theft Auto V', type='text'), html.Label('Streams', style={'fontSize': 20, 'marginTop': 30}), dcc.Slider( id='stream_num', min=5, max=50, marks={i*5: str(i*5) for i in range(1, 11)}, value=10, ), dcc.Graph(id='jobs_barchart', style={'marginTop': 30, 'marginBottom': 30}), html.Button(id='submit-button', n_clicks=0, children='Go', style={ 'display': 'block', 'margin': 'auto', 'width': 100, 'textAlign': 'center', 'fontSize': 20, 'fontFamily': 'inherit', 'color': '#00ff7f', 'border-color': '#00ff7f', 'border-width': 2, } ), ]) @app.callback( Output(component_id='jobs_barchart', component_property='figure'), [Input(component_id='submit-button', component_property='n_clicks'),], [State(component_id='game_name', component_property='value'), State(component_id='stream_num', component_property='value'),], ) def update_figure(n_clicks, game, n_streams): # if no game specified, get top streams across all games if game == '': payload = {'first': n_streams} r = requests.get(streams_url, headers=headers, params=payload).json() else: payload = {'name': game} r = requests.get(games_url, headers=headers, params=payload).json() game_id = r['data'][0]['id'] payload = {'game_id': game_id, 'first': n_streams} r = requests.get(streams_url, headers=headers, params=payload).json() return { 'data': [ go.Bar( x=[d['user_name']], y=[d['viewer_count']], text=d['title'], opacity=0.8, name=d['user_name'], hoverinfo='x+y+text', showlegend=False ) for d in r['data'] ], 'layout': go.Layout( xaxis={'title': 'Streams'}, yaxis={'title': 'Viewers'}, hovermode='closest', title = game + ' Streams with Most Current Viewers', #font=dict(family='AR DESTINE, Arial, Helvetica, sans serif'), ) } if __name__ == '__main__': app.run_server(debug=True) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import dash from dash.dependencies import Input, Output, State import dash_html_components as html import dash_core_components as dcc import plotly.graph_objs as go import requests # twitch api info client_id = 'xxxxxxxxxxxxxxxxxxxx' headers = {'Client-ID': client_id} games_url = 'https://api.twitch.tv/helix/games' streams_url = 'https://api.twitch.tv/helix/streams' app = dash.Dash(__name__) app.layout = html.Div(style={'backgroundColor': '#4b367b', 'padding': 30}, children=[ html.H1( children='Top Twitch Streams', style={'textAlign': 'center'} ), html.Div( children='Select a game and number of top streams you wish to see', style={'textAlign': 'center', 'fontSize': 20} ), html.Label('Game', style={'fontSize': 20}), dcc.Input(id='game_name', value='Grand Theft Auto V', type='text'), html.Label('Streams', style={'fontSize': 20, 'marginTop': 30}), dcc.Slider( id='stream_num', min=5, max=50, marks={i*5: str(i*5) for i in range(1, 11)}, value=10, ), dcc.Graph(id='jobs_barchart', style={'marginTop': 30, 'marginBottom': 30}), html.Button(id='submit-button', n_clicks=0, children='Go', style={ 'display': 'block', 'margin': 'auto', 'width': 100, 'textAlign': 'center', 'fontSize': 20, 'fontFamily': 'inherit', 'color': '#00ff7f', 'border-color': '#00ff7f', 'border-width': 2, } ), ]) @app.callback( Output(component_id='jobs_barchart', component_property='figure'), [Input(component_id='submit-button', component_property='n_clicks'),], [State(component_id='game_name', component_property='value'), State(component_id='stream_num', component_property='value'),], ) def update_figure(n_clicks, game, n_streams): # if no game specified, get top streams across all games if game == '': payload = {'first': n_streams} r = requests.get(streams_url, headers=headers, params=payload).json() else: payload = {'name': game} r = requests.get(games_url, headers=headers, params=payload).json() game_id = r['data'][0]['id'] payload = {'game_id': game_id, 'first': n_streams} r = requests.get(streams_url, headers=headers, params=payload).json() return { 'data': [ go.Bar( x=[d['user_name']], y=[d['viewer_count']], text=d['title'], opacity=0.8, name=d['user_name'], hoverinfo='x+y+text', showlegend=False ) for d in r['data'] ], 'layout': go.Layout( xaxis={'title': 'Streams'}, yaxis={'title': 'Viewers'}, hovermode='closest', title = game + ' Streams with Most Current Viewers', #font=dict(family='AR DESTINE, Arial, Helvetica, sans serif'), ) } if __name__ == '__main__': app.run_server(debug=True)

Now that we have an app that’s working locally, let’s take a moment to discuss styling.

App Styling

One thing to note is that, while there is some styling in the code itself for certain margins, font size, background color, etc, a lot of styling comes from this CSS stylesheet.

In the Dash User Guide examples, you’ll find that they use this stylesheet, by Chris P, the cofounder of Plotly and author of Dash. For my own stylesheet, I simply copied Chris’ and made a few changes, such as adding the custom font AR Destine.

To test out the CSS and have it applied to your app running locally, within your main project folder you’ll need to make an “assets” folder where you place the stylesheet, custom font file, and any other external resources. See the youtube video for more details on this, or read through Dash’s article on external resources.

So now that we’ve got the app working and styled as we like, we need to figure out how to get it hosted online so it’s publicly accessible and we can embed it in our hypothetical gaming blog.

Web Hosting with Python Anywhere

There are a number of different options for hosting your code online, but if you’re new to this and don’t have a preference, I recommend trying out Python Anywhere. I’ve had good experiences with them so far, they’re very user-friendly, and they have a free “Beginner” account option (details in bottom table, far right column).

The free account option comes with limitations, but it’ll still allow us to get the app up and running. See the youtube video LINK for more details on configuring your app in Python Anywhere, but here’s a quick summary of the process:

Open Bash console, create virtual environment, install packages If while installing packages you get errors relating to allowed disk usage (which can often happen with a free account), try installing requests or plotly first, instead of dash (which installs every package you need at once) to break up the installation process into smaller chunks You can also try cleaning up unnecessary files

Upload files

Add a new app, choose manual configuration

Specify the source code folder and virtual environment

Configure the WSGI file by replacing its contents with the following code, altered for your situation:

import sys path = '/home/grayson/twitch_apps' if path not in sys.path: sys.path.append(path) from top_twitch_streams import app application = app.server 1 2 3 4 5 6 7 8 import sys path = '/home/grayson/twitch_apps' if path not in sys.path: sys.path.append(path) from top_twitch_streams import app application = app.server Reload the app

Access the app at username.pythonanywhere.com

Check the Error log files in the Web tab if something goes wrong

If you get lost, check out Python Anywhere’s Help section

Once you’ve got the app hosted successfully, it’s time for the final step, embedding it in your website.

Embedding the Web App

Python Anywhere assigns our app a url where we can access it. If you have a free account, that url is yourusername.pythonanywhere.com. For example, you can see the web app embedded below at grayson.pythonanywhere.com. But it would be inconvenient for users to have to click a link and navigate to another site to use the app, and it would also look a lot cooler to have the app right there on our website.

This is the simplest step. You can easily embed the app anywhere you want using an iframe element. And all that entails is simply adding the following line to your web page:

<iframe src="https://username.pythonanywhere.com" width="100%" height="900" frameborder="0" sandbox="allow-same-origin allow-scripts"></iframe>

The Twitch app is embedded this same way below. If for some reason it’s not working (which could be due to browser settings or add-ons), just watch the youtube video to see how it works or better yet get your own version of it up and running. A great exercise would be to add some new features to it or improve existing ones. Maybe polish up its look too.

Wrapping Up

As you can see, building web apps can be within the reach of even relative beginners. If you’d like to build an app of your own design, check out the Dash gallery page or this other Dash app collection to get a feel for what’s possible.

This curated list of Dash resources and these Dash recipes are worth reviewing as well.

And if all else fails you can leave a comment below or ask the Dash or Python Anywhere communities for help.

So feel free to let me know if you have any questions, and consider subscribing to my newsletter below and youtube channel. Until next time.