Using Python in the browser

Snake Charmer

© Photo by Godwin Angeline Benjo on Unsplash

© Photo by Godwin Angeline Benjo on Unsplash

Author(s):

PyScript lets you use your favorite Python libraries on client-side web pages.

While there are some great Python web server frameworks such as Flask, Django, and Bottle, using Python on the server side adds complexity for web developers. To use Python on the web, you also need to support JavaScript on client-side web pages. To address this problem, some Python-to-JavaScript translators, such as JavaScripthon, Js2Py, and Transcrypt, have been developed.

The Brython (which stands for Browser Python) project [1] took the first big step in offering Python as an alternative to JavaScript by offering a Python interpreter written in JavaScript. Brython is a great solution for Python enthusiasts, because it's fast and easy to use. However, it only supports a very limited selection of Python libraries.

PyScript [2] offers a new, innovative solution to the Python-on-a-web-page problem by allowing access to many of the Python Package Index (PyPI) repository libraries. The concept behind PyScript is a little different. It uses Pyodide, which is a Python interpreter for the WebAssembly (Wasm) virtual machine. This approach offers Python within a virtual environment on the web client.

In this article, I will introduce PyScript with some typical high school or university engineering examples. I will also summarize some of the strengths and weakness that I've found while working with PyScript.

Getting Started

PyScript doesn't require any special software on either the server or client; all the coding is done directly on the web page. For PyScript to run, it needs three things (Figure 1):

  • a definition in the header for the PyScript CSS and JS links,
  • a <py-config> section to define the Python packages to load, and
  • a <py-script> section for the Python code.
Figure 1: The main components of a PyScript web page.

In Figure 1, the <py-script> section uses terminal=true (the default) to enable Python print() statements to go directly to the web page. A little bit later, I'll show you how to put PyScript data into HTML tags.

Figure 2 shows the running web page. This math example performs the Python SymPy simplify function on a complex equation to reduce the equation to its simplest form. The pprint() (pretty print) function outputs the equation into a more presentable format on the page's py-terminal element (the black background section shown in Figure 2).

Figure 2: PyScript using the Python SymPy library.

Debugging code is always an issue. The web browser will highlight some general errors in the PyScript pages. To see more detailed Python errors, right-click on the page and select the Inspect option and then click on the Console heading. Figure 3 shows a very typical error: a print() function missing a closing quote character.

Figure 3: Debug PyScript with the browser's Inspect option.

Calling PyScript Functions

In the previous example, PyScript was called just once, which is similar to how a JavaScript <script> block is executed when it is embedded within a web page's <body> section.

There are several ways to call a PyScript function. You can use the traditional JavaScript approach of adding a function reference within a tag reference as shown in the following button example:

<button py-click="my_pyfunc()" id="button1">Call Pyscript</button>

PyScript supports a wide range of actions. For the button, a click event is defined with the py-click option, but other actions such as a double-click (py-dblclick) or a mouseover (py-mouseover) event could also be added.

Listing 1 shows a button click action that calls a function, current_time(), to print the present time into a PyScript terminal section (Figure 4).

Listing 1

Button Click Action

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Current Time</title>
    <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
    <script defer src="https://pyscript.net/latest/pyscript.js"></script>
  </head>
  <body>
    <h1>Py-click to call a Pyscript Function</h1>
    <!-- add py-click into the button tag -->
    <button py-click="current_time()" id="get-time" class="py-button">Get current time</button>
    <py-script>
      import datetime
      # this function is called from a button
      def current_time():
        print( datetime.datetime.now())
    </py-script>
  </body>
</html>
Figure 4: Button click to call a PyScript function.

A more Pythonic approach to calling a PyScript function is available with the @when API. The syntax for this is:

<py-script>
  from pyscript import when
  # define id and action,
  # then next line is the function
  @when("click", selector="#button1")
  def my_pyfunc():
    print("Button 1 pressed")
</py-script>

You can also use the @when function to refresh an HTML tag, which I cover in the next section.

A Calendar Example

Now I'll provide a calendar example (Listing 2) that uses a button and PyScript to replace the contents of an HTML tag. To keep things simple, the Python's calendar output will be left as ASCII and an HTML <pre> tag will be used (Figure 5).

Listing 2

PyScript Yearly Calendar

01 <html>
02     <head>
03       <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
04       <script defer src="https://pyscript.net/latest/pyscript.js"></script>
05           <title>Pyscript Calendar Example</title>
06     </head>
07
08   <body>
09     <h1>Pyscript Calendar Example</h1>
10     Move Years:
11     <button id="btn_back"> Back </button>
12     <button id="btn_forward"> Forward </button>
13     <pre id="calzone"></pre>
14
15 <py-script>
16 from pyscript import when
17 import calendar
18
19 thisyear = 2023
20 display(calendar.calendar(thisyear), target="calzone" )
21
22 @when("click", selector="#btn_back")
23 def back_year():
24         global thisyear
25         thisyear -= 1
26         display(calendar.calendar(thisyear), target="calzone", append=False )
27
28 @when("click", selector="#btn_forward")
29 def forward_year():
30         global thisyear
31         thisyear += 1
32         display(calendar.calendar(thisyear), target="calzone", append=False )
33
34 </py-script>
35 </body>
36 </html>
Figure 5: PyScript calendar with @when functions.

The calendar page has Back and Forward buttons (lines 10-11) and a <pre> section (line 12).

In the <py-script> section, the when and calendar libraries are imported on lines 15-17. These two libraries are part of the base PyScript/Python that is loaded into Pyodide, so a <py-config> section is not needed.

Like calling PyScript functions, there are multiple ways to read and write web content. PyScript has a built-in display() function that is used to write to HTML tags (lines 20, 26, and 32). The syntax for the display() function is:

display(*values, target="tag-id", append=True)

The *value can be a Python variable or an object like a Matplotlib figure.

The @when function (lines 22 and 28) connects the Back and Forward button clicks to the functions back_year() and forward_year().

PyScript with JavaScript Libraries

In many cases you'll want to use JavaScript libraries along with PyScript. For example, you might want to include JavaScript prompts or alert messages for your page. To access a JavaScript library, add the line:

from js import some_library

Listing 3 shows the code to import the alert and prompt libraries, then prompts the user for their name, and finally displays an alert message with the entered name (Figure 6).

Listing 3

JavaScript Libraries with PyScript

<py-script>
  # Use a JS library to show a prompt and alert message
  from js import alert, prompt
  # Ask your name, then show it back
  name = prompt("What's your name?", "Anonymous")
  alert(f"Hi:, {name}!")
</py-script>
Figure 6: You can use JavaScript libraries in PyScript.

Reading and Plotting a Local CSV File

For a final, more challenging example, I'll use PyScript to read a local CSV file into a pandas dataframe and then use Matplotlib to plot a bar chart (Figure 7).

Figure 7: Read and plot a local CSV file as a bar chart.

For security reasons, web browsers cannot access local files without the user's authorization. To allow PyScript to access a local file, you need to do three key things. To start, you need to configure a page with an <input type="file"> tag. To call a file-picker dialog with a CSV filter, enter:

<input type="file" id="myfile" name="myfile" accept=".csv">

Next, you must define an event listener to catch a change in the <input> file. For this step, two libraries need to be imported, and an event listener needs to be configured as shown in Listing 4.

Listing 4

Defining an Event Listener

from js import document
from pyodide.ffi.wrappers import add_event_listener
# Set the listener to look for a file name change
e = document.getElementById("myfile")
add_event_listener(e, "change", process_file)

Finally, you need to import the JavaScript FileReader and the PyScript asyncio libraries as follows:

from js import FileReader
import asyncio

The FileReader object is used to read in the CSV file's content. The asyncio library creates background event processing to allow functions to complete successfully without timing or delay issues.

Listing 5 shows the full code for reading and plotting a local CSV file. In Listing 5, pay particular attention to:

  • defining a <py-config> section for the pandas and Matplotlib (PyPI) libraries (lines 9-11) and
  • creating an async function (process_file(event)).

Note, the async function is launched from the add_event_listener (line 51) when the user selects a file.

Listing 5

PyScript CSV File to Bar Chart

01 <!DOCTYPE html>
02 <html lang="en">
03   <head>
04     <title>Pyscript CSV to Plot</title>
05     <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
06     <script defer src="https://pyscript.net/latest/pyscript.js"></script>
07     <title>Local CSV File to Matplotlib Chart</title>
08     <!-- Include the Pandas and Matplotlib packages -->
09     <py-config>
10       packages = [ "pandas", "matplotlib" ]
11     </py-config>
12   </head>
13   <body>
14
15     <h1>Pyscript: Input Local CSV File and Create a Bar Chart</h1>
16     <label for="myfile">Select a CSV file to graph:</label>
17     <input type="file" id="myfile" name="myfile" accept=".csv"><br>
18
19     <div id="lineplot"> </div>
20     <pre id="print_output"> </pre>
21     <py-script output="print_output">
22 import pandas as pd
23 import matplotlib.pyplot as plt
24 from io import StringIO
25 import asyncio
26 from js import document, FileReader
27 from pyodide.ffi.wrappers import add_event_listener
28
29 # Process a new user selected CSV file
30 async def process_file(event):
31     fileList = event.target.files.to_py()
32     for f in fileList:
33         data = await f.text()
34         # the CSV file is read as large string
35         # use StringIO to pass info into Panda dataframe
36         csvdata = StringIO(data)
37         df = pd.DataFrame(pd.read_csv(csvdata, sep=","))
38         print("DataFrame of:", f.name, "\n",df)
39
40         # create a Matplotlib figure with headings and labels
41         fig, ax = plt.subplots(figsize=(16,4))
42         plt.bar(df.iloc[:,0], df.iloc[:,1])
43         plt.title(f.name)
44         plt.ylabel(df.columns[1])
45         plt.xlabel(df.columns[0])
46         # Write Mathplot figure to div tag
47         pyscript.write('lineplot',fig)
48
49 # Set the listener to look for a file name change
50 e = document.getElementById("myfile")
51 add_event_listener(e, "change", process_file)
52
53 </py-script>
54 </body>
55 </html>

The CSV file is read into a variable (line 34), and then the StringIO function allows the data to be passed into a pandas dataframe (lines 36 and 37). Line 38 outputs the dataframe to a py-terminal element:

print("DataFrame of:", f.name, "\n",df)

This example only presents bar charts for the first two rows of data (lines 42-45), but the code would be modified to do line plots for multiple rows of data. Line 47 sends the Matplotlib figure to the page's <div id="lineplot"> element:

pyscript.write('lineplot',fig)

Although somewhat complex, this example only took 30 lines of Python code. Good future projects could include adding options for sorting, grouping, and customized plots. It's important to note that PyScript can also be used to save files to a local machine.

Summary

Using Python libraries such as pandas, SymPy, or Matplotlib on a client page can be a very useful feature. It's also nice that these PyScript pages don't require Python on the client machine.

While working with PyScript, I found two issues. The call-up is very slow (especially compared to Brython pages). In addition, I often got tripped up with Python indentation when I was cutting and pasting code. Overall, however, I was very impressed with PyScript, and I look forward to seeing where the project goes.

Infos

  1. Brython: https://brython.info/
  2. PyScript: https://pyscript.net/

The Author

You can investigate more neat projects by Pete Metcalfe and his daughters at https://funprojects.blog.