A First Look at PyScript: Python in the Web Browser

A First Look at PyScript: Python in the Web Browser

PyScript is a brand-new framework that caused a lot of excitement when Peter Wang, the CEO and co-founder of Anaconda, Inc., revealed it during his keynote speech at PyCon US 2022. Although this project is just an experiment in an early phase of development, people on social media seem to have already fallen in love with it. This tutorial will get you up to speed with PyScript, while the official documentation is still in the making.

In this tutorial, you’ll learn how to:

  • Build interactive front-end apps using Python and JavaScript
  • Run existing Python code in the web browser
  • Reuse the same code on the back-end and the front-end
  • Call Python functions from JavaScript and the other way around
  • Distribute Python programs with zero dependencies

To get the most out of this tutorial, you should ideally have some experience with JavaScript and front-end programming in general. At the same time, you’ll be able to follow along just fine even if you’ve never done any web development before. After all, that’s the whole idea behind PyScript!

Disclaimer: PyScript Is an Experimental Project!

This tutorial is coming to you only a few weeks after the official announcement of PyScript. At the time of writing, you could find the following warning prominently displayed on the framework’s home page:

Please be advised that PyScript is very alpha and under heavy development. There are many known issues, from usability to loading times, and you should expect things to change often. We encourage people to play and explore with PyScript, but at this time we do not recommend using it for production. (Source)

This can’t be stressed enough. Before you get started, be prepared for things not to work as presented in this tutorial. Some things may not work at all by the time you read this, while some problems might have already been addressed in one way or another.

That’s not surprising, given PyScript’s relatively short history. Because it’s open-source software, you can take a peek at its Git commit history on GitHub. When you do, you’ll find that Fabio Pliger from Anaconda, who’s the creator and technical lead of PyScript, made the initial commit on February 21, 2022. That’s just over two months before it was publicly announced to the world on April 30!

That’s where things stand with PyScript. If you’re ready to take the risk and would like to give this framework a try, then keep reading.

Getting Started With PyScript

By the end of this section, you’ll have a bird’s-eye view of the framework and its building blocks, including how they work together to bring Python into your browser. You’ll know how to cherry-pick the needed files and host PyScript locally without depending on an Internet connection.

Wrap Your Head Around PyScript

You might be asking yourself what exactly PyScript is. The name is probably a clever attempt at marketing it as a replacement for JavaScript in the browser, but such an interpretation wouldn’t give you the complete picture. Here’s how PyScript is currently being advertised on its Twitter profile:

PyScript - programming for the 99% (Source)

One of the goals of PyScript is to make the Web a friendly place for anyone wanting to learn to code, including kids. The framework achieves that goal by not requiring any installation or configuration process beyond your existing text editor and a browser. A side effect is that PyScript simplifies sharing your work with others.

When you look at PyScript’s README file, you’ll find the following summary and a longer description:

PyScript is a Pythonic alternative to Scratch, JSFiddle, and other “easy to use” programming frameworks, with the goal of making the web a friendly, hackable place where anyone can author interesting and interactive applications.

(…)

PyScript is a meta project that aims to combine multiple open technologies into a framework that allows users to create sophisticated browser applications with Python. It integrates seamlessly with the way the DOM works in the browser and allows users to add Python logic in a way that feels natural both to web and Python developers (Source)

Scratch is a relatively straightforward visual programming language that children learn at school to build simple games and funny animations. JSFiddle is JavaScript’s online editor commonly used to demonstrate a given problem’s solution on forums like StackOverflow.

Furthermore, PyScript’s home page contains this tagline:

PyScript is a framework that allows users to create rich Python applications in the browser using HTML’s interface and the power of Pyodide, WASM, and modern web technologies. The PyScript framework provides users at every experience level with access to an expressive, easy-to-learn programming language with countless applications. (Source)

In other words, PyScript allows you to use Python, with or without JavaScript, to build interactive websites that don’t necessarily have to communicate with a server. The main benefit here is that you can leverage your existing knowledge of Python to enter the world of front-end development, lowering the entry barrier and making it more accessible. But there are many other benefits of using PyScript that you’ll learn about later.

On a slightly more technical level, PyScript is a single-page application (SPA) written in TypeScript using the Svelte framework, styled with Tailwind CSS, and bundled with rollup.js. According to one of the comments in an early Git commit, the project was based on a template mentioned in a blog post by Sascha Aeppli, which combines those tools.

PyScript wouldn’t be possible without building on top of a recent version of Pyodide—a CPython interpreter compiled with emscripten to WebAssembly, enabling Python to run in the browser. PyScript provides a thin abstraction layer over Pyodide by encapsulating the required boilerplate code, which you’d otherwise have to type yourself using JavaScript.

If you’re not familiar with Pyodide and WebAssembly, then click to expand the collapsible section below:

There have been many attempts at running Python code in the browser, with varying levels of success. The primary challenge was that, until recently, JavaScript has been the only programming language that web browsers could understand.

There were a few extra hoops to jump through to run Python code in the browser. For example, Transcrypt transpiles a piece of Python code to an analogous snippet of JavaScript code, whereas Brython is a streamlined Python interpreter implemented in JavaScript. These and similar tools are less than ideal because they rely on something that’s pretending to be a genuine Python runtime.

Pyodide is different as it takes advantage of WebAssembly, a fairly new standard supported by modern web browsers and meant for enabling near-native code execution speed. You can think of WebAssembly as the second “programming language” that browsers now understand.

WebAssembly is a full-fledged virtual machine capable of running a portable bytecode that you can target by compiling the source code of almost any programming language. However, when you look at the list of languages compatible with WebAssembly, then you won’t find Python there. Can you guess why?

Python is an interpreted language, which doesn’t come with a standard compiler that could target WebAssembly. What Pyodide did instead was to compile the entire CPython interpreter into WebAssembly, letting it sit in the browser and interpret Python code just as if it were your regular Python interpreter.

When you forget about certain limitations of the browser’s sandbox and its security policies, there should be no functional difference between running the code through CPython or Pyodide and only a modest difference in performance. Pyodide and WebAssembly make the web browser an excellent place to distribute your Python programs.

To make the integration between Python and the web browser even more straightforward, PyScript defines several Web Components and custom elements, such as <py-script> and <py-button>, that you can embed directly into your HTML. If you’re bothered by the hyphen in these custom tag names, then don’t blame PyScript. The HTML specification enforces it to avoid name conflicts between Web Components and future HTML elements.

Without further ado, it’s time to write your first Hello, World! in PyScript!

Write Your First “Hello, World!” in PyScript

The quickest way to get started with PyScript is by creating a minimal HTML5 document, saving it in a local file such as hello.html, and leveraging the two required files hosted on PyScript’s home page:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hello, World!</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
</head>
<body>
  <py-script>print("Hello, World!")</py-script>
</body>
</html>

The first file, pyscript.css, provides default styling for PyScript’s visual components that you’ll explore later, as well as the loader splash screen. The second file, pyscript.js, contains JavaScript that bootstraps the Python runtime and adds custom elements like <py-script>, which can hold Python instructions such as a call to the print() function.

With this setup, you don’t need to start a web server to access your HTML content. See for yourself! Go ahead, save that HTML document to a local file, and open it directly in your favorite web browser:

PyScript's Hello, World!
PyScript's Hello, World!

Congratulations! You’ve just made your first PyScript application, which will work on any modern web browser, even on an early Chromebook, without the need for installing a Python interpreter. You can literally copy your HTML file onto a USB thumb drive and hand it over to a friend, and they’ll be able to run your code even if they don’t have Python installed on their machine.

Next up, you’ll learn what just happened when you opened that file in the web browser.

Fetch the Python Runtime From the Internet

When you open your HTML file in a web browser, it’ll take a few seconds to load before showing Hello, World! in the window. PyScript must fetch a dozen additional resources from jsDelivr CDN, JavaScript’s free Content Delivery Network for open-source projects. Those resources comprise the Pyodide runtime, which weighs over twenty megabytes in total when uncompressed.

Fortunately, your browser will cache most of those resources in memory or on disk so that, in the future, the load time will be noticeably faster. You’ll also be able to work offline without depending on your Internet connection as long as you’ve opened your HTML file at least once.

Relying on a CDN to deliver your dependencies is undoubtedly convenient, but it can be brittle at times. There were known cases of CDNs going down in the past, which caused outages of big online businesses. Because PyScript is on the bleeding edge, the CDN always serves the latest alpha build, which can sometimes bring a breaking change. Conversely, a CDN may occasionally need time to keep up with GitHub, so it might be serving outdated code.

Wouldn’t it be better to always request a specific version of PyScript?

Download PyScript for Offline Development

If you don’t want to rely on PyScript’s hosting service, then you’ll need to download all the files required to run Python in the browser and host them yourself. For development purposes, you can start a local HTTP server built right into Python by issuing the following command in a directory with your files to host:

Windows PowerShell
PS> python -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...
Shell
$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

By default, it’ll start a server listening for HTTP requests on all network interfaces, including the localhost, and the port number 8000. You can tweak both the address and the port number with optional arguments if needed. This will let you access your PyScript application at http://localhost:8000/hello.html, for example.

However, before you can do that, you’ll need to download pyscript.css, pyscript.js, and pyscript.py to a folder where your HTML document is located. To do so, you can use the Wget command-line tool, which has its equivalent in PowerShell on Windows, or download the files manually:

Windows PowerShell
PS> foreach ($ext in "css", "js", "py") {
>> wget "https://pyscript.net/alpha/pyscript.$ext" -o "pyscript.$ext"
>> }
Shell
$ wget https://pyscript.net/alpha/pyscript.{css,js,py}

This will download all three files in one go. The helper Python module contains the necessary glue code for PyScript and Pyodide. You need to download pyscript.py because the bootstrap script will try to fetch it from its own domain address, which is going to be your localhost address.

Don’t forget to update the CSS and JavaScript paths in your HTML so that they point to the local files instead of the ones hosted online:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hello, World!</title>
  <link rel="stylesheet" href="/pyscript.css" />
  <script defer src="/pyscript.js"></script>
</head>
<body>
  <py-script>print("Hello, World!")</py-script>
</body>
</html>

Here, you assume the placement of those resource files next to your HTML file, but you could also create one or more subfolders for those assets to keep things organized.

You’re almost there. But, if you navigate your browser to your local server now, then it’ll still try to fetch some resources from the CDN and not your local HTTP server. You’ll fix that in the next section.

Download a Specific Pyodide Release

Now that you’ve made PyScript work offline, it’s time to follow similar steps for Pyodide. In the earliest days of PyScript, the URL with Pyodide was hard-coded, but the developers recently introduced another custom element called <py-config>, which allows you to specify a URL with the desired version of Pyodide:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hello, World!</title>
  <link rel="stylesheet" href="/pyscript.css" />
  <script defer src="/pyscript.js"></script>
  <py-config>
    - autoclose_loader: true
    - runtimes:
      -
        src: "/pyodide.js"
        name: pyodide-0.20
        lang: python
  </py-config>
</head>
<body>
  <py-script>print("Hello, World!")</py-script>
</body>
</html>

The content inside of this optional tag is a piece of YAML configuration. You can use the src attribute to provide either a URL with a concrete Pyodide version hosted online or a local file that you’ll download in a bit. In the code block above, "/pyodide.js" indicates a path relative to your local HTTP server’s root address, which would expand to http://localhost:8000/pyodide.js, for example.

To obtain an up-to-date list of the remaining files that your browser would download from the CDN or load from its cache, you can head over to the web development tools and switch to the Network tab before refreshing the page. However, chances are that you’ll eventually need a few more files, or their names might change in the future, so checking the network traffic would quickly become a nuisance.

For the purposes of development, it’s probably more convenient to download all the files of a Pyodide release and decide later which of those your application really needs. So, if you don’t mind downloading a few hundred megabytes, then grab the release tarball from GitHub and extract it into your Hello, World! application’s folder:

Windows PowerShell
PS> $VERSION='0.20.0'
PS> $TARBALL="pyodide-build-$VERSION.tar.bz2"
PS> $GITHUB_URL='https://github.com/pyodide/pyodide/releases/download'
PS> wget "$GITHUB_URL/$VERSION/$TARBALL"
PS> tar -xf "$TARBALL" --strip-components=1 pyodide
Shell
$ VERSION='0.20.0'
$ TARBALL="pyodide-build-$VERSION.tar.bz2"
$ GITHUB_URL='https://github.com/pyodide/pyodide/releases/download'
$ wget "$GITHUB_URL/$VERSION/$TARBALL"
$ tar -xf "$TARBALL" --strip-components=1 pyodide

Don’t worry if the commands above don’t work on your operating system. You can download the archive and extract the contents of its pyodide/ subfolder manually.

As long as everything goes fine, you should have at least these fourteen files in your application’s folder:

hello-world/
│
├── distutils.tar
├── hello.html
├── micropip-0.1-py3-none-any.whl
├── packages.json
├── packaging-21.3-py3-none-any.whl
├── pyodide.asm.data
├── pyodide.asm.js
├── pyodide.asm.wasm
├── pyodide.js
├── pyodide_py.tar
├── pyparsing-3.0.7-py3-none-any.whl
├── pyscript.css
├── pyscript.js
└── pyscript.py

As you can see, PyScript is an amalgam of Python, JavaScript, WebAssembly, CSS, and HTML. In practice, you’ll be doing the bulk of your PyScript programming using Python.

The approach you’ve taken in this section gives you much more granular control over the versions of Pyodide and the underlying Python interpreter. To check which Python versions are available through Pyodide, you can see the changelog. For example, Pyodide 0.20.0, used in this tutorial, was built on top of CPython 3.10.2.

When in doubt, you can always verify the Python version running in your browser yourself, as you’ll learn next.

Verify Your Pyodide and Python Versions

To check your Pyodide version, all you need is a single line of code. Head back to your code editor and replace the Hello, World! code with the following snippet:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hello, World!</title>
  <link rel="stylesheet" href="/pyscript.css" />
  <script defer src="/pyscript.js"></script>
  <py-config>
    - autoclose_loader: true
    - runtimes:
      -
        src: "/pyodide.js"
        name: pyodide-0.20
        lang: python
  </py-config>
</head>
<body>
  <py-script>import pyodide_js; print(pyodide_js.version)</py-script>
</body>
</html>

It relies on the pyodide_js module, which gets automatically injected into PyScript. You can use it to access Pyodide’s JavaScript API directly from Python in case PyScript doesn’t provide its own abstraction layer for a given feature.

Checking your Python version in PyScript looks the same as in the standard CPython interpreter:

HTML
<py-script>import sys; print(f"Python {sys.version}")</py-script>

You import the sys module from the standard library to check the sys.version constant and then print it with an f-string. When you refresh the page in the web browser and let it reload, then it should produce a string that starts with something like this:

Python 3.10.2 (main, Apr 9 2022, 20:52:01) […]

That’s what you’d typically see when you run an interactive Python interpreter in the command line. In this case, Pyodide was not that far behind the latest CPython release, which was 3.10.4 at the time of writing this tutorial.

By the way, have you noticed the semicolon (;) in the examples above? In Python, a semicolon separates multiple statements that appear on a single line, which can be useful when you write a one-liner script or when you’re constrained, for example, by the timeit module’s setup string.

The use of semicolons in Python code is rare and generally frowned upon by seasoned Pythonistas. However, this unpopular symbol helps sidestep a problem with Python’s significant whitespace, which can sometimes get messy in PyScript. In the next section, you’ll learn how to deal with block indentation and Python code formatting embedded in HTML.

Dealing With Python Code Formatting

When you embed a piece of CSS, JavaScript, or even an SVG image in an HTML document, there’s no risk of the web browser misinterpreting their associated code, because they’re all examples of free-form languages, which ignore extra whitespace. You can format your code however you like in those languages—for example, by removing line breaks—without losing any information. That’s what makes JavaScript’s minification possible.

Unfortunately, this isn’t true for Python, which follows the off-side rule for its syntax, where every space character counts. Because PyScript is such a novel technology, most of today’s automatic code formatters will likely make the wrong assumptions and destroy your Python code contained within the <py-script> tag by collapsing the significant whitespace. If that happens, you might end up with an error similar to this one:

Python Traceback
Traceback (most recent call last):
  ...
  File "<exec>", line 2
    print(f"Python {sys.version}")
IndentationError: unexpected indent

In this case, Pyodide fails to parse your Python code snippet, which was embedded in HTML, due to a broken indentation. You’ll see this exception and the associated traceback in the document’s body as well as in the web developer’s console.

But that’s not all. If you declare a string literal in your Python code that happens to look like an HTML tag, then the browser will recognize it as an HTMLUnknownElement and strip it away, leaving only the text inside. Consider parsing XML as an example:

HTML
<py-script>
import xml.etree.ElementTree as ET
ET.fromstring("<person>John Doe</person>")
</py-script>

The code above uses the ElementTree API from Python’s standard library to parse a string containing an XML-formatted data record about a person. However, the actual parameter passed into the function will only be "John Doe" without the surrounding tags.

Notice that from the web browser’s perspective, <person> looks like another HTML tag nested under the <py-script> parent element. To avoid such ambiguity, you can replace the angle brackets (< and >) with their encoded counterparts known as HTML entities:

HTML
<py-script>
import xml.etree.ElementTree as ET
ET.fromstring("&lt;person&gt;John Doe&lt;/person&gt;")
</py-script>

The &lt; entity stands for the “less than” (<) character, whereas &gt; replaces the “greater than” (>) character. Character entities allow browsers to render text literally, when it would otherwise be interpreted as HTML elements. This works in PyScript, but it doesn’t solve the indentation problem.

Unless you’re only playing around with PyScript, then you’re usually better off extracting your Python code to a separate file instead of mixing it with HTML. You can do so by specifying the optional src attribute on the <py-script> element, which looks similar to the standard <script> tag meant for JavaScript:

HTML
<py-script src="/custom_script.py"></py-script>

This will load and immediately run your Python script as soon as the page is ready. If you only wish to load a custom module into the PyScript runtime in order to make it available for importing, then check out dependency management with <py-env> in the next section.

You can have multiple <py-script> tags on your page as long as they appear in the page’s <head> or <body>. PyScript will put them on a queue and run them in sequence.

Now you know how to run your Python code in the browser with PyScript. However, most practical applications require one or more dependencies. In the next section, you’ll find out how to leverage Python’s existing “batteries,” third-party libraries published on PyPI or elsewhere, and your own Python modules.

Managing Python Dependencies in PyScript

So far, you’ve seen the <py-script> and <py-config> custom tags provided by the framework. Another element that you’ll often want to use is <py-env>, which helps manage your project dependencies, similar to Python’s pip tool.

Modules Missing From the Python Standard Library

Python comes with batteries included, which means that many of its standard library modules already solve common problems that you might face during software development. You’ll find that most of these modules are available in Pyodide and PyScript out of the box, letting you import and use them right away. For instance, you’ve seen a code snippet that took advantage of the xml.etree package to parse an XML document.

However, there are a few notable exceptions due to the constraints of a web browser and an effort to reduce the download size. Anything irrelevant to a browser environment was removed from the current Pyodide release. In particular, these include but are not limited to the following modules and packages:

You can check the full list of removed packages on Pyodide’s documentation page.

Apart from that, a few packages were kept as placeholders, which might eventually get proper support once WebAssembly evolves in the future. Today, you can import them, as well as modules like urllib.request that depend on them, but they won’t work:

In general, you can’t start new processes, threads, or open low-level network connections. That being said, you’ll learn about some mitigations later in this tutorial.

What about using external libraries in PyScript that you’d typically install with pip into your virtual environment?

Third-Party Libraries Bundled With Pyodide

Pyodide is a spin-off project of the now discontinued Iodide parent project kicked off by Mozilla. Its goal was to provide the tools for doing scientific computing in the web browser in a way similar to Jupyter Notebooks but without the need for communicating with the server to run Python code. As a result, researchers would be able to share and reuse their work more easily for the price of limited computational power.

Because PyScript is a wrapper around Pyodide, you can access a number of popular third-party libraries that were compiled for WebAssembly with Pyodide, even those with parts written in C and Fortran. For instance, you’ll find the following packages there:

The actual list is much longer and isn’t limited to libraries designed strictly for data science. There were close to a hundred libraries bundled with Pyodide at the time of writing this tutorial. You can check Pyodide’s official documentation for the complete list or head over to the packages/ folder in the corresponding GitHub repository to see the latest status.

Even though these external libraries are part of a Pyodide release, they’re not automatically loaded into your Python runtime. Remember that each individual Python module must be fetched over the network into your web browser, which takes valuable time and resources. When PyScript starts, your environment has only the bare minimum necessary to interpret Python code.

To import modules that aren’t present in the Python standard library, you must explicitly request them by declaring their names in a <py-env> element:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Sine Wave</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
</head>
<body>
  <py-env>
    - matplotlib
    - numpy
  </py-env>
  <py-script>
import matplotlib.pyplot as plt
import numpy as np

time = np.linspace(0, 2 * np.pi, 100)
plt.plot(time, np.sin(time))
plt
  </py-script>
</body>
</html>

The <py-env> element contains a YAML list with the names of libraries to fetch on demand. When you look at the Network tab in your web development tools, you’ll notice that the browser downloads NumPy and Matplotlib from the CDN server or your local HTTP server hosting Pyodide. It also pulls several transitive dependencies required by Matplotlib, amounting to over twelve megabytes in size!

Alternatively, you can install dependencies programmatically in Python, as PyScript exposes Pyodide’s micropip tool, which is a streamlined version of pip:

HTML
<py-script>
# Run this using "asyncio"

async def main():
    await micropip.install(["matplotlib", "numpy"])

await loop.run_until_complete(main())

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 100)
plt.plot(x, np.sin(x))
plt
</py-script>

The micropip module and the implicit loop variable, which represents the default asynchronous event loop, are available in the global namespace in PyScript without the need for importing them.

Note that you must call the module’s .install() method asynchronously through Python’s asyncio package to hook into the web browser’s Fetch API leveraged by micropip. Also, while the imports style guide recommends putting your import statements at the top of the file, they have to come after the await statement here to make sure that the required libraries have been fetched and installed by micropip first.

If Pyodide doesn’t find the required library in its bundled packages, then it’ll make an attempt to download it from the Python Package Index (PyPI). However, not all libraries will work that way due to certain runtime constraints.

Pure-Python Wheels Downloaded From PyPI

Say you wanted to create a PyScript application that parses XML using the untangle library, which is neither bundled with Pyodide nor distributed with the standard library. You add the following declaration to the <py-env> element and reload your page in the browser:

HTML
<py-env>
  - untangle
</py-env>

Pyodide contacts PyPI to fetch the associated JSON metadata and concludes that the library wasn’t built and uploaded with the expected Python wheel format. It’s only available as a source distribution (sdist), which might require an additional compilation step. It doesn’t matter if the library contains only pure Python code. In this case, Pyodide expects a wheel archive, which it can extract and start using immediately.

Slightly disappointed, you try your luck with another XML-parsing library called xmltodict, which converts documents to Python dictionaries rather than objects:

HTML
<py-env>
  - xmltodict
</py-env>

This time, the library’s metadata indicates that a pure-Python wheel archive is available, so Pyodide goes ahead and fetches it. If the library had its own dependencies, then Pyodide would try to fetch them too. However, the dependency resolution mechanism implemented in micropip is extremely rudimentary. From now on, the xmltodict library becomes importable in your PyScript application.

However, if you tried fetching a non-pure-Python library, such as a binary driver for the PostgreSQL database, then Pyodide would refuse to load it into your runtime. Even if it was built as Python wheels for various platforms, none of them would be suitable for WebAssembly. You can view the wheels uploaded to PyPI for any given library by clicking Download files on the corresponding page.

To sum up, a third-party library listed in <py-env> must be a pure-Python one and be distributed using the wheel format to be picked up, unless it’s already been built for WebAssembly and bundled with Pyodide. Getting a custom non-pure-Python library into PyScript is tricky.

C Extension Modules Compiled for WebAssembly

Many Python libraries contain bits of code written in C or other languages for performance gains and to leverage specific system calls unavailable in pure Python. There are a few ways to interface with such code, but it’s a common practice to wrap it in a Python C extension module that can be compiled to the native code of your platform and loaded dynamically at runtime.

Using the emscripten compiler lets you target WebAssembly instead of particular computer architecture and operating system. However, doing so is not an easy feat. Even if you know how to build a Python wheel for the Pyodide runtime and you’re not intimidated by the process, PyScript’s <py-env> tag always expects either a pure-Python wheel or a package bundled with Pyodide.

To install a wheel containing WebAssembly code, you can call Pyodide’s loadPackage() function using its Python interface, pyodide_js, mentioned earlier. You could also use Pyodide’s API in JavaScript directly, but it would start an independent runtime instead of hooking up to one already created by PyScript. As a result, your custom module with WebAssembly code wouldn’t be visible in PyScript.

Loading custom C extension modules may eventually become more straightforward. Until then, your best bet seems to be patiently waiting for Pyodide to ship with the desired library. Alternatively, you could build your own Pyodide runtime from source code with the extra cross-compiled libraries. There’s a command-line tool called pyodide-build, which automates some of the steps involved.

For now, you may want to stick with custom Python modules written by hand.

Custom Python Modules and Data Files

You can use <py-env> or micropip to make your custom modules importable in PyScript applications. Suppose you made a helper module called waves.py, which is sitting in an src/ subfolder:

Python
# src/waves.py

import numpy as np

def wave(frequency, amplitude=1, phase=0):
    def _wave(time):
        return amplitude * np.sin(2 * np.pi * frequency * time + phase)

    return _wave

The name of your module uses a plural form to avoid clashing with the wave module in the standard library, which is used for reading and writing Waveform Audio File Format (WAV). Your module defines a single function called wave(), which returns a closure. The inner function _wave(), on which the closure is based, uses NumPy to generate a pure sine wave with the given frequency, amplitude, and phase.

Before you can import your module in a <py-script> tag, from either an inline or sourced script, you’ll need to fetch it into your web browser with <py-env> by specifying a special paths attribute in YAML:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Sine Wave</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
</head>
<body>
  <py-env>
    - matplotlib
    - numpy
    - paths:
        - src/waves.py
  </py-env>
  <py-script>
import matplotlib.pyplot as plt
import numpy as np
import waves

time = np.linspace(0, 2 * np.pi, 100)
plt.plot(time, waves.wave(440)(time))
plt
  </py-script>
</body>
</html>

The file path is relative to your HTML page. As before, you must host your files through a web server due to the CORS policy, which doesn’t allow getting additional files through the file:// protocol.

On the upside, it’s worth noting that while you can’t load a directory into PyScript, you can abuse the paths attribute to load virtually any file into it. That includes data files like textual CSV files or a binary SQLite database:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Loading Data</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
</head>
<body>
  <py-env>
    - paths:
        - data/people.csv
        - data/people.sql
  </py-env>
  <py-script>
with open("people.csv") as file:
    print(file.read())
  </py-script>
  <py-script>
import sqlite3

with sqlite3.connect("people.sql") as connection:
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM people")
    for row in cursor.fetchall():
        print(row)
  </py-script>
</body>
</html>

Notice that when you fetch your files with <py-env>, then you lose information about their original directory structure, as all the files end up in a single target directory. When you open a file, you only specify its name without a path, which means that your files must be uniquely named. You’ll learn how to mitigate that problem later by writing to a virtual file system in Pyodide.

All right. Now that you know how to get your Python code or someone else’s into PyScript, you should learn to work with the framework more effectively.

Emulating Python REPL and Jupyter Notebook

A quick and fun way to learn Python or any of its underlying libraries is by trying them out in a live interpreter session, also known as the Read-Eval-Print-Loop (REPL). By interacting with the code, you can explore what functions and classes are available and how you’re supposed to use them. It’s no different with PyScript.

Because PyScript’s environment takes a while to load, refreshing the page every time you’ve edited your code isn’t going to cut it. Fortunately, the framework comes with yet another custom element called <py-repl>, which allows you to execute small snippets of code without reloading the page. You can have as many of those as you like in your HTML, leaving them empty or prepopulating them with some initial Python code:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>PyScript REPL</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
  <py-env>
    - matplotlib
    - numpy
  </py-env>
</head>
<body>
  <py-repl>
import matplotlib.pyplot as plt
import numpy as np
  </py-repl>
  <py-repl>
def wave(frequency, amplitude=1, phase=0):
    def _wave(time):
        return amplitude * np.sin(2 * np.pi * frequency * time + phase)

    return _wave
  </py-repl>
  <py-repl></py-repl>
</body>
</html>

Unlike the elements that you’ve already explored, <py-repl> has a visual representation that builds on top of the CodeMirror editor, which supports themeable syntax highlighting, autocompletion, code folding, and more. As long as you’ve included the default CSS stylesheet provided by PyScript, this new element should look something like this when rendered in the browser:

PyScript's REPL Resembling a Jupyter Notebook
PyScript's REPL Resembling a Jupyter Notebook

Each instance of <py-repl> resembles a cell in a Jupyter Notebook. You can type multiple lines of Python code into a cell and click the green play button in its bottom-right corner to have your code executed. You may also use the Shift+Enter key combination to achieve a similar effect. Below are a few additional keyboard shortcuts provided by CodeMirror:

Action macOS Windows and Linux
Search Cmd+F Ctrl+F
Select scope Cmd+I Ctrl+I
Select line Ctrl+L Alt+L
Delete line Cmd+Shift+K Ctrl+Shift+K
Insert line below Cmd+Enter Ctrl+Enter
Move selection up Alt+Up Alt+Up
Move selection down Alt+Down Alt+Down
Go to matching bracket Cmd+Shift+\ Ctrl+Shift+\
Indent Cmd+] Ctrl+]
Dedent Cmd+[ Ctrl+[

The table above illustrates the default keymap, which can be tweaked to mimic Emacs, Vim, or Sublime Text.

If the last line in your cell contains a valid Python expression, then PyScript will append its representation just below the cell. For example, it may render a graph plotted with Matplotlib. When you view such a web page in the browser, it does start to look like a Jupyter Notebook.

However, unlike in a Jupyter Notebook, executing a <py-repl> won’t insert a new cell by default. If you’d like to enable such behavior, then set the element’s auto-generate attribute to true:

HTML
<py-repl auto-generate="true"></py-repl>

Now, when you run the code in such a cell for the first time, it’ll insert another one at the bottom of the page. The subsequent runs won’t, though. This new cell will have the auto-generate attribute itself.

Another default behavior of the <py-repl> element is appending new <div> containers for the Python output and tracebacks. You can optionally redirect the standard output and standard error streams to separate, custom elements on the page, letting you design an IDE-like environment in the browser:

PyScript Playground IDE
Custom PyScript Playground in the Web Browser

Here’s the code of the web page depicted in the screenshot above:

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>PyScript Playground</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
  <style>
    body {
      background-color: #eee;
    }
    .container {
      display: grid;
      grid-template-columns: 1fr 1fr;
      grid-auto-rows: minmax(200px, auto);
      grid-gap: 1em;
      margin-top: 1em;
    }
    .container > div:first-child {
      grid-row: 1/3;
    }
    .container > div {
      background-color: #fff;
      box-shadow: 0px 5px 10px #ccc;
      padding: 10px;
    }
  </style>
  <py-env>
    - matplotlib
    - numpy
  </py-env>
</head>
<body>
  <div class="container">
    <div>
      <py-repl std-out="output" std-err="errors"></py-repl>
    </div>
    <div>
      <b>Output</b><hr>
      <div id="output"></div>
    </div>
    <div>
      <b>Errors and Warnings</b><hr>
      <div id="errors"></div>
    </div>
  </div>
</body>
</html>

Use the std-out and std-err attributes to specify the corresponding target element IDs. If you’d like to combine both streams and dump them into a single element, then you can use the output attribute instead. The same attributes apply to the <py-script> element that you saw earlier.

The remaining elements currently supplied by PyScript are visual components, also known as widgets, which simplify working with HTML and Python. You’ll explore them now.

Exploring PyScript’s Visual Components

The visual components that PyScript provides are convenient wrappers for HTML elements, which might be helpful for beginners who don’t have much experience with web development, especially on the client side. However, those widgets are highly experimental, have limited functionality, and could be removed in the future. Sometimes, they load too quickly, before Pyodide is fully initialized, which causes errors. Use them at your own risk!

PyTitle

With the PyTitle element, you can quickly add a textual header to your web page, which will appear in uppercase letters and be centered horizontally, provided that you linked the default CSS stylesheet that comes with PyScript. Here’s how you can use this widget in your HTML code:

HTML
<py-title>PyScript Playground</py-title>

It’s a purely visual component with no additional attributes or behaviors, and it’ll only accept plain text content between its opening and closing tags.

PyBox

PyBox is a container element that can arrange its children using the CSS Flexbox layout model in the horizontal direction. It currently uses Tailwind CSS width classes, such as w-1, w-1/2, or w-full, to define the column widths.

You can specify the individual widths of your child elements through the optional widths attribute of the <py-box> parent:

HTML
<py-box widths="2/3;1/6;1/6">
  <div>Wide Column</div>
  <div>Narrow Column</div>
  <div>Narrow Column</div>
</py-box>

Notice that you only provide the part of a CSS class name that comes after the w- prefix, and you delimit the widths using a semicolon (;). In the example above, the first <div> would take up two-thirds of the available space in the row (⅔), while the other two elements together would account for one-third (⅙ + ⅙ = ⅓). If you skip the widths attribute, then all children will be stretched equally to have the same size.

PyButton

PyButton is the first interactive widget in PyScript that lets you call a Python function in response to a user action, like clicking a mouse button. To handle JavaScript’s click event, define an inline function named on_click() inside of your <py-button> element:

HTML
<py-button label="Click me" styles="btn big">
def on_click(event):
    print(event)
</py-button>

The function is a callback that takes a PointerEvent object and returns nothing. While it’s not possible to attach a callback defined elsewhere, you can always delegate to some helper function. If you don’t like the default look and feel of a PyButton, then you can overwrite it with one or more CSS classes through the styles attribute.

Another event that a PyButton supports is the focus event, which you can listen to by defining the on_focus() function. It’ll receive the FocusEvent object as an argument.

Sadly, due to the fast pace of development of PyScript and Pyodide, the latter introduced a breaking change in version 0.19 by fixing a memory leak, which unexpectedly made PyButton stop working. The same problem applies to PyInputBox, which you’ll learn about next. Hopefully, both widgets will have been repaired by the time you read this.

PyInputBox

PyInputBox is the last widget currently available in PyScript, and it wraps the HTML’s input element and lets you listen to the keypress event on it:

HTML
<py-inputbox>
def on_keypress(event):
    print(event)
</py-inputbox>

Note that the keypress event was deprecated in favor of the keydown event, which PyInputBox doesn’t support yet. Therefore, you might find it less cumbersome to access and manipulate standard HTML elements directly from PyScript without the extra abstraction layers that might break.

Using PyScript to Find and Manipulate HTML Elements

WebAssembly doesn’t currently allow direct interaction with the Document Object Model (DOM), which would typically give you access to the HTML’s underlying elements. Instead, you must use JavaScript to query and traverse the DOM tree. Accessing JavaScript objects in Python is possible thanks to Pyodide’s proxy objects, which facilitate the translation between both languages. There are two ways to use them for DOM manipulation in PyScript.

PyScript’s Adapter for JavaScript Proxy

For trivial use cases, when you’re in a hurry and don’t mind the lack of extra bells and whistles, you might give PyScript’s Element class a try. It’s already in your global namespace, meaning that you don’t have to import it. The class lets you find HTML elements only by ID and modify their content to a limited extent.

Consider the following sample HTML document:

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>DOM API in PyScript</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
  <style>
    .crossed {
      text-decoration: line-through;
    }
  </style>
</head>
<body>
  <div id="shopping-list">
    <p>Shopping List</p>
    <ul>
      <li class="crossed">milk</li>
      <li class="crossed">eggs</li>
      <li>bread</li>
    </ul>
    <input id="new-item" type="text" placeholder="Add new item">
    <button>Add</button>
  </div>
  <py-repl></py-repl>
</body>
</html>

It’s a shopping list with some crossed-off items and an input box at the bottom for adding new items to buy. The <py-repl> element will let you test and play around with the PyScript API interactively when you open this page in your web browser.

Say you want to determine which new shopping item a user has typed into that input box, and then you want to clear it. Here’s a piece of Python code that does just that when placed in either a <py-script> or <py-repl> tag:

Python
input_new_item = Element("new-item")
print(input_new_item.value)
input_new_item.clear()

First, you grab a reference to the <input> element by calling the Element() class constructor with the HTML id attribute "new-item" as an argument. Then, you print the element’s .value property, and call .clear() to reset its value. You’ll need to type something into the input box before seeing any result.

The Element class has other valuable attributes, which are read-only, and a few methods that you can call to change the element’s style or state:

Member Description
.element A proxy object for JavaScript’s HTML element
.id The string value of an HTML element’s id attribute if it exists
.value The string value of an HTML element’s .value property if it exists
.add_class() Add one or more CSS classes
.remove_class() Remove one or more CSS classes
.write() Update the .innerHTML property or add a brand new <div> element
.clear() Clear the .value or .innerHTML property
.clone() Clone the element and insert its deep copy into the DOM tree
.select() Find a descendant Element using a CSS selector

At any time, you can drill down to the actual HTMLElement proxy wrapped by PyScript through the .element attribute. The .select() method is an interesting one because it lets you find a descendant element nested in your container element using any CSS selector and not just a plain ID. For example, you can find the first crossed-off list item and remove the crossed CSS class from it while adding it to the last element on that list:

Python
shopping_list = Element("shopping-list")
shopping_list.select("ul > li.crossed").remove_class("crossed")
shopping_list.select("ul > li:last-child").add_class("crossed")

When you run this code snippet through your PyScript REPL, then the milk will no longer be crossed off, but the bread will be. The next time you run it, the eggs will become uncrossed too.

That’s about everything you can do with PyScript’s Element class. For the ultimate control over the DOM tree and your device, you’ll want to reach for the proxy object itself.

Pyodide’s JavaScript Proxy

Start by importing the js module provided by Pyodide in your PyScript code:

Python
import js

print(js.window.innerWidth)
print(js.document.title)
js.console.log("Hello from Python!")

This module opens JavaScript’s global namespace up, revealing your functions and variables, as well as objects implicitly provided by the web browser. These include the window and document objects, as well as the HTMLElement data type, which you’ll be most interested in when manipulating the DOM.

Note that PyScript already imports a few things from the js module into the global namespace in Python for your convenience. In particular, the console and document objects are already there:

HTML
<py-script>
console.log("This looks like JavaScript, but it's Python!")
console.log(document.title)
</py-script>

You don’t have to import them yourself or prefix their names with the js module anymore, which makes Python look a lot like JavaScript when you squint your eyes.

Okay, what about adding new items to the shopping list from the previous section, based on the value of the input box? Here’s how you can do this by leveraging the DOM API in PyScript:

Python
input_new_item = document.querySelector("#new-item")

if input_new_item.value:
    child = document.createElement("li")
    child.innerText = input_new_item.value

    parent = document.querySelector("#shopping-list ul")
    parent.appendChild(child)

    input_new_item.value = ""

This should look familiar to anyone who’s built web user interfaces using pure JavaScript before. You query the entire HTML document for the <input> element using a CSS selector. If the user has typed some value into that input box, then you create a new list item element (<li>) and populate it using the .innerText property. Next, you find the parent <ul> element and append the child. Finally, you clear the input box.

You can go even crazier with using the document object in PyScript. For instance, it’s possible to write code that looks like a hybrid of Python and JavaScript by combining the APIs of both:

Python
from random import choice, randint

colors = ["azure", "beige", "khaki", "linen", "skyblue"]
for li in document.querySelectorAll("#shopping-list li"):
    if not li.hasAttribute("data-price"):
        li.dataset.price = randint(1, 15)
    li.classList.toggle("crossed")
    li.style.backgroundColor = choice(colors)

In this case, you import a few functions from the Python standard library to generate random values, and you also take advantage of the Python syntax, for example, to iterate over a JavaScript array wrapped in a proxy object. The loop goes through the shopping list items, conditionally assigns a random price to each, toggles their CSS class, and picks a random background color.

The DOM API exposed by the web browser is way too rich to fit in this tutorial, but feel free to expand the collapsible section below for a quick reference of the most important attributes and methods of the HTMLElement class:

Each HTML element has the following attributes in JavaScript:

Attribute Description
.classList A list-like object with element’s CSS class names
.className A string with the element’s class attribute value
.dataset A dictionary-like object with custom key-value pairs
.innerHTML A string with the HTML content between the opening and closing tags
.innerText A string with the textual content between the opening and closing tags
.style An object containing CSS style declarations
.tagName An uppercase name of the HTML element tag

Most of these attributes support reading and writing. In addition to them, you’ll also find attributes related to the DOM tree traversal relative to the current element:

Attribute Description
.parentElement The parent of this element
.children A list-like object of the immediate child elements
.firstElementChild The first child element
.lastElementChild The last child element
.nextElementSibling The next element on the same tree level
.previousElementSibling The previous element on the same tree level

To manipulate generic attributes not listed here, you can call the following methods on an element:

Method Description
.hasAttribute() Check if the element has a given attribute
.getAttribute() Return the value of a given attribute
.setAttribute() Assign or overwrite the value of an attribute
.removeAttribute() Delete an attribute from the element

Once you have an object reference to an element, you may want to narrow down the search space by looking up its descendants with a CSS selector while ignoring the rest of the branches in the document:

Method Description
.querySelector() Return a nested element matching a given CSS selector
.querySelectorAll() Return a list-like object of nested elements matching a given CSS selector

These two methods work the same way as their counterparts in the document object. Additionally, you’ll be able to modify the structure of your DOM tree with the following handy methods:

Method Description
.cloneNode() Create a shallow or deep copy of the element
.appendChild() Add a new element as the last child
.insertBefore() Insert a new element before the specified child
.removeChild() Remove a given child element
.replaceChild() Replace a given child with another element

Finally, you’ll want to add behaviors to your element by making it respond to DOM events, usually caused by user actions:

Method Description
.addEventListener() Register a callback for a given event type
.removeEventListener() Unregister a callback for a given event type

To use those, you’ll need to know about creating callback functions in PyScript, which you’ll learn about next.

To make the shopping list usable, it needs to become interactive. But how do you handle DOM events, such as mouse clicks, in PyScript? The answer is that you register a callback function!

Python Event Callback Proxy

You’ve already written the code that creates a new list item based on a value from the input box. However, it’s not tied to any visual component on the page, as you have to manually run it through the <py-repl> element. It would be nice if clicking on the Add button triggered that code for you.

First, you’ll need to encapsulate your existing code in a function that takes a DOM event as an argument. In JavaScript, you can skip arguments that you don’t need, but since you’re writing a Python function, its signature must be more explicit:

Python
def on_add_click(event):
    # ...

The event argument will be unused and ignored, but that’s okay. Assuming you have your function in place, it’s time to tell the browser when to call it. You do that by adding an event listener to an element that should trigger an action. In this case, that element is the button, which you can find with an appropriate CSS selector, and the event name is "click":

Python
button = document.querySelector("button")
button.addEventListener("click", on_add_click)

The second line in the code block above adds the listener, which will seemingly work without emitting any errors. However, as soon as you try clicking your button in the browser, nothing will happen.

The web browser expects a JavaScript callback, but you give it a Python function. To fix this, you can create a proxy object out of your Python callable by using a relevant function from the pyodide module:

Python
from pyodide import create_proxy

button = document.querySelector("button")
button.addEventListener("click", create_proxy(on_add_click))

That’s it! You can now replace your <py-repl> tag with a <py-script> one that has the complete Python code, and you can start enjoying your first interactive PyScript application. Here’s the resulting HTML document structure:

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>DOM API in PyScript</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
  <style>
    .crossed {
      text-decoration: line-through;
    }
  </style>
</head>
<body>
  <div id="shopping-list">
    <p>Shopping List</p>
    <ul>
      <li class="crossed">milk</li>
      <li class="crossed">eggs</li>
      <li>bread</li>
    </ul>
    <input id="new-item" type="text" placeholder="Add new item">
    <button>Add</button>
  </div>
  <py-script>
from pyodide import create_proxy

def on_add_click(event):
    input_new_item = document.querySelector("#new-item")

    if input_new_item.value:
        child = document.createElement("li")
        child.innerText = input_new_item.value

        parent = document.querySelector("#shopping-list ul")
        parent.appendChild(child)

        input_new_item.value = ""

button = document.querySelector("button")
button.addEventListener("click", create_proxy(on_add_click))
  </py-script>
</body>
</html>

The equivalent JavaScript code would take about the same space due to calling the same API. On the one hand, using the proxy objects adds more characters, but on the other hand, Python doesn’t need curly brackets like JavaScript to delimit every code block.

Apart from finding and manipulating HTML elements with the DOM API, you can do more cool stuff related to the web browser in PyScript. In the next section, you’ll take a closer look at a few remaining elements of the front-end programming that you’d typically have to code in JavaScript.

Interfacing With the Web Browser From Python

In traditional front-end development, you rely on the JavaScript engine as much as on the set of APIs provided by your execution environment, which are two separate parts of the equation. While the core of JavaScript is relatively small and behaves rather consistently across various vendors, the landscape of APIs can be vastly different depending on the target platform.

You can use JavaScript for the server-side code running in Node.js, client-side code executed in the web browser, or even code powering mobile apps. By the way, it’s technically possible to run PyScript in Node.js on the back-end, leveraging some of its distinctive APIs. However, in this section, you’ll focus on the browser APIs.

Cookies

Working with HTTP cookies in vanilla JavaScript without the aid of any external library can get clunky because it forces you to parse a long string containing the complete information of all cookies for a given domain. Conversely, defining a new cookie in JavaScript requires concatenating a string with the desired attributes manually. Fortunately, PyScript lets you use Python, which comes with batteries included.

One of the batteries at your fingertips is the http.cookies module, which can do the cookie string parsing and encoding for you. To parse the JavaScript’s document.cookie attribute, use the SimpleCookie class available in Python:

Python
from http.cookies import SimpleCookie

for cookie in SimpleCookie(document.cookie).values():
    print(f"🍪 {cookie.key} = {cookie.value}")

The Python helper class is capable of parsing the entire string with cookie attributes that a server would typically send to the browser. However, in JavaScript, you only get to see the names and the corresponding values of non-HttpOnly cookies without their attributes.

Use the code snippet below to specify a new cookie that will expire in a year for the current domain. As a good citizen, you should also explicitly set the SameSite attribute to avoid relying on the default behavior, which varies across browsers and their versions:

Python
from datetime import timedelta
from http.cookies import SimpleCookie

cookies = SimpleCookie()
cookies["dark_theme"] = "true"
cookies["dark_theme"] |= {
    "expires": int(timedelta(days=365).total_seconds()),
    "samesite": "Lax"
}

document.cookie = cookies["dark_theme"].OutputString()

Admittedly, the interface of this module looks quite dated and doesn’t conform to a Pythonic style, but it gets the job done. When you run this code and check your browser cookies, you’ll see a new dark_theme cookie set to true. You can fiddle with the number of seconds to see if your browser removes the cookie after a specified time.

You can optionally generate a Set-Cookie header string with additional attributes, some of which aren’t allowed in JavaScript:

Python
from http.cookies import SimpleCookie

cookies = SimpleCookie()
cookies["session_id"] = "1ddb897c43fc5f1b773cc5af6cfbe4cf"
cookies["session_id"] |= {
    "httponly": True,
    "secure": True,
    "samesite": "Strict",
    "domain": "your-domain.org",
    "path": "/",
    "max-age": str(int(timedelta(hours=8).total_seconds())),
}

print(cookies["session_id"])

The above code would produce the following long line of text representing an HTTP header, which has been divided to fit on your screen and avoid horizontal scrolling:

HTTP
Set-Cookie: session_id=1ddb897c43fc5f1b773cc5af6cfbe4cf;
⮑ Domain=your-domain.org;
⮑ HttpOnly;
⮑ Max-Age=28800;
⮑ Path=/;
⮑ SameSite=Strict;
⮑ Secure

You can’t set such a cookie yourself because it specifies certain attributes that aren’t allowed in JavaScript, but it may be possible to receive one from a web server when fetching some data.

Fetch API

When making HTTP requests from the web browser in JavaScript, you’re constrained to several security policies, which don’t give you as much freedom as you might be used to as a back-end developer. Furthermore, JavaScript’s inherently asynchronous model doesn’t play well with Python’s synchronous functions for making network connections. As a result, modules like urllib.request or socket are of no use in PyScript.

Pyodide recommends writing HTTP clients in terms of web APIs such as the promise-based Fetch API. To make calling that API from Python more straightforward, Pyodide provides the pyfetch() wrapper function, which works in an asynchronous context.

If you’re looking to make a REST API request, for example, to authenticate yourself with a username and a password, then you can call pyfetch(), which has a signature similar to JavaScript’s fetch() function:

Python
 1# Run this using "asyncio"
 2
 3import json
 4
 5from pyodide.http import pyfetch
 6from pyodide import JsException
 7
 8async def login(email, password):
 9    try:
10        response = await pyfetch(
11            url="https://reqres.in/api/login",
12            method="POST",
13            headers={"Content-Type": "application/json"},
14            body=json.dumps({"email": email, "password": password})
15        )
16        if response.ok:
17            data = await response.json()
18            return data.get("token")
19    except JsException:
20        return None
21
22token = await loop.run_until_complete(
23    login("eve.holt@reqres.in", "cityslicka")
24)
25print(token)

The word asyncio, which appears in a comment at the top of the code fragment above, tells PyScript to run this code asynchronously so that you can await the event loop at the bottom. Remember that you can put this magic word anywhere in your code to trigger the same action, which is currently like casting a spell. Perhaps there will eventually be a more explicit way to toggle this behavior—for example, through an attribute on the <py-script> tag.

When you call the login() coroutine with an email address and a password, you make an HTTP POST request to a fake API hosted online. Notice that you serialize the payload to JSON in Python using the json module instead of JavaScript’s JSON object.

You can also use pyfetch() to download files and then save them to the virtual file system provided by emscripten in Pyodide. Note that these files will only be visible in your current browser session through the I/O interface, but you won’t find them in the Downloads/ folder on your disk:

Python
# Run this using "asyncio"

from pathlib import Path

from pyodide.http import pyfetch
from pyodide import JsException

async def download(url, filename=None):
    filename = filename or Path(url).name
    try:
        response = await pyfetch(url)
        if response.ok:
            with open(filename, mode="wb") as file:
                file.write(await response.bytes())
    except JsException:
        return None
    else:
        return filename

filename = await loop.run_until_complete(
    download("https://placekitten.com/500/900", "cats.jpg")
)

Unless you want to save the file under a different name, you use the pathlib module to extract the filename from a URL that your function will return. The response object returned by pyfetch() has an awaitable .bytes() method, which you use to save the binary content to a new file.

Later, you can read the downloaded file from the virtual file system and display it on an <img> element in HTML:

Python
import base64

data = base64.b64encode(open(filename, "rb").read()).decode("utf-8")
src = f"data:image/jpeg;charset=utf-8;base64,{data}"
document.querySelector("img").setAttribute("src", src)

You’ll need to transform the raw bytes into text using Base64 encoding, and then format the resulting string as a data URL before assigning it to the src attribute of the image element.

As an alternative, you can use a completely synchronous function in PyScript to fetch data over the network. The only catch is that open_url() can’t read binary data:

Python
from pyodide.http import open_url

pep8_url = "https://raw.githubusercontent.com/python/peps/main/pep-0008.txt"
pep8_text = open_url(pep8_url).getvalue()

import json
user = json.load(open_url("https://jsonplaceholder.typicode.com/users/2"))

svg = open_url("https://www.w3.org/Icons/SVG/svg-logo-v.svg")
print(svg.getvalue())

This first call to open_url() fetches the original text of the PEP 8 document, which you store in a variable. The second call communicates with a REST API endpoint that returns a user object in the JSON format, which you then deserialize to a Python dictionary. The third call downloads the official SVG logo, which you can render in your browser since SVG is a text-based format.

When you fetch data from the Internet, you usually want to store it for later access. Browsers offer a few web storage areas to choose from, depending on your information’s desired scope and lifetime. Local storage is your best option if you want to store data persistently.

Local Storage

In the following code snippet, you greet the user with a welcome message shown in an alert box, addressing them by the name that was previously saved in the browser’s local storage. If the user is visiting your page for the first time, then you display a prompt dialog asking for the user’s name:

Python
from js import alert, prompt, localStorage, window

if not (name := localStorage.getItem("name")):
    name = prompt("What's your name?", "Anonymous")
    localStorage.setItem("name", name)

alert(f"Welcome to {window.location}, {name}!")

To get a hold of the local storage, you import the localStorage reference from JavaScript and use its .getItem() and .setItem() methods to persist key-value pairs. You also take advantage of the Walrus operator (:=), introduced in Python 3.8, to make the code more concise, and you display the URL address of the browser window.

Okay, how about something more exciting?

Sensor API

Web browsers running on mobile devices like tablets or smartphones expose the Sensor API, which gives JavaScript programmers access to the device’s accelerometer, ambient light sensor, gyroscope, or magnetometer if it’s equipped with one. Additionally, some sensors can be emulated in software by combining the signals from multiple physical sensors, reducing noise. A gravity sensor is a good example.

You can check out a live demo illustrating the use of the gravity sensor in PyScript. Make sure to open the link on a mobile device. As soon as you change the orientation of your phone or tablet, you’ll see one of these messages displayed on the screen:

  • Horizontal counterclockwise
  • Horizontal clockwise
  • Vertical upright
  • Vertical upside down
  • Screen up
  • Screen down
  • Tilted

In case your device doesn’t come with a gravity sensor or you’re accessing the site over an unencrypted connection, you’ll get notified about that through a pop-up window. Alternatively, you might just see a blank screen.

To hook into a sensor on your device, you’re going to need to write a bit of a JavaScript glue code because PyScript doesn’t currently export the Pyodide instance that it creates into the JavaScript’s global namespace. If it did, then you could grab one and access the Python proxy objects in JavaScript with slightly less hassle. For now, you’ll go the other way around by calling a JavaScript function from Python.

Create a new index.html file and keep adding content to it. First, define a <script> tag in your HTML web page and fill it in with the following JavaScript code:

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>PyScript Gravity Sensor</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
</head>
<body>
  <script>
    function addGravityListener(callback) {
      if ("GravitySensor" in window) {
        const sensor = new GravitySensor({frequency: 60})
        sensor.addEventListener("reading",
          () => callback(sensor.x, sensor.y, sensor.z)
        )
        sensor.start()
      } else {
        alert("Gravity sensor unavailable")
      }
    }
  </script>
</body>
</html>

The function takes a callback, which will be a JavaScript proxy for your Python function. It then checks if your browser supports the GravitySensor interface and creates a new sensor instance with a sampling frequency of sixty times per second. A single sensor reading is a three-dimensional vector representing the direction and magnitude of gravity.

Next up, implement and register the Python callback in the browser using PyScript:

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>PyScript Gravity Sensor</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
</head>
<body>
  <span></span>
  <script>
    function addGravityListener(callback) {
      if ("GravitySensor" in window) {
        const sensor = new GravitySensor({frequency: 60})
        sensor.addEventListener("reading",
          () => callback(sensor.x, sensor.y, sensor.z)
        )
        sensor.start()
      } else {
        alert("Gravity sensor unavailable")
      }
    }
  </script>
  <py-script>
from js import addGravityListener
from pyodide import create_proxy

span = document.querySelector("span")

def callback(x, y, z):
    span.innerText = f"{x:.1f}, {y:.1f}, {z:.1f}"

addGravityListener(create_proxy(callback))
  </py-script>
</body>
</html>

Assuming there’s a <span> element somewhere on your page, you find its reference using a CSS selector and then write a formatted string with the three components of the gravity vector into it after taking a sensor reading. Note the need for wrapping your Python callback in a JavaScript proxy before registering it as a listener.

Knowing the direction of the gravity vector will tell you something about your phone’s orientation, which can be useful when you want to take a level picture or to detect when you picked up the device from the desk, for example. The magnitude of the gravity vector is the Earth’s acceleration, which you might use as a rough estimate of altitude.

To make this example more interesting, go ahead and use NumPy to detect various orientations of the device:

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>PyScript Gravity Sensor</title>
  <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
  <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
  <py-env>
    - numpy
  </py-env>
</head>
<body>
  <span></span>
  <script>
    function addGravityListener(callback) {
      if ("GravitySensor" in window) {
        const sensor = new GravitySensor({frequency: 60})
        sensor.addEventListener("reading",
          () => callback(sensor.x, sensor.y, sensor.z)
        )
        sensor.start()
      } else {
        alert("Gravity sensor unavailable")
      }
    }
  </script>
  <py-script>
from js import addGravityListener
from pyodide import create_proxy
import numpy as np

span = document.querySelector("span")

def callback(x, y, z):
    span.innerText = orientation(x, y, z)

def orientation(x, y, z):
    gravity = np.array([x, y, z])
    v = list(np.round(gravity / np.linalg.norm(gravity)).astype(int))
    if v == [ 1,  0,  0]: return "Horizontal counterclockwise"
    if v == [-1,  0,  0]: return "Horizontal clockwise"
    if v == [ 0,  1,  0]: return "Vertical upright"
    if v == [ 0, -1,  0]: return "Vertical upside down"
    if v == [ 0,  0,  1]: return "Screen up"
    if v == [ 0,  0, -1]: return "Screen down"
    return "Tilted"

addGravityListener(create_proxy(callback))
  </py-script>
</body>
</html>

You add the <py-env> declaration to fetch NumPy into your PyScript environment. Then, you import the library at the top of your existing <py-script> tag and make the callback delegate the processing to a helper function. The new orientation() function normalizes and rounds your gravity vector to compare it against a few unit vectors along the axes in the device coordinate system.

If your device doesn’t support the gravity sensor, then try to identify other sensors that work, then think of an idea for another application and adapt the code shown in this example accordingly. You can share your cool idea in the comments section below!

Timer Functions

JavaScript often uses so-called timer functions to schedule a callback to run once in the future or periodically every specified number of milliseconds. The latter can be useful for animating content on the page or polling the server for the latest snapshot of rapidly changing data.

If you intend to delay the execution of a one-off function—for example, to display a reminder notification or a pop-up window after a specific time—then consider using create_once_callable() to create a proxy object. Pyodide will automatically dispose of it when done:

Python
from js import alert, setTimeout
from pyodide import create_once_callable

setTimeout(
  create_once_callable(
    lambda: alert("Reminder: Meeting in 10 minutes")
  ),
  3000  # Delay in milliseconds
)

You use the setTimeout() function from JavaScript, which expects a callable object, such as a Python lambda function wrapped in a proxy, and the number of milliseconds to wait before running your callable. Here, you display an alert box with a reminder after three seconds.

You’ll notice that PyScript prints a numeric value onto the HTML document after running the code above. It’s the return value of the setTimeout() function, which provides a unique identifier of the timeout, which you can optionally cancel with the corresponding clearTimeout() function:

Python
from js import alert, setTimeout, clearTimeout
from pyodide import create_once_callable

timeout_id = setTimeout(
  create_once_callable(
    lambda: alert("Reminder: Meeting in 10 minutes")
  ),
  3000  # Delay in milliseconds
)

clearTimeout(timeout_id)

In this case, you cancel the timeout using its unique identifier immediately after scheduling your callback, so it never runs.

There’s an analogous pair of JavaScript functions named setInterval() and clearInterval() that work largely the same. However, the calls to your callback will be made repeatedly at intervals—for example, every three seconds minus the execution time of your function. If your function takes longer to execute, then it’ll run as soon as possible next time, without delay.

To use setInterval() in PyScript, you’ll need to remember to wrap your callback function with a call to create_proxy() instead of create_once_callable() to prevent Pyodide from disposing of it after the first run:

Python
from random import randint

from js import alert, setInterval, setTimeout, clearInterval
from pyodide import create_once_callable, create_proxy

def callback():
    r, g, b = randint(0, 255), randint(0, 255), randint(0, 255)
    document.body.style.backgroundColor = f"rgb({r}, {b}, {b})"

interval_id = setInterval(create_proxy(callback), 1000)

_ = setTimeout(
    create_once_callable(
        lambda: clearInterval(interval_id)
    ),
    10_000
)

A few things are going on here. You register a callback to run every second, which sets the document’s background to a random color. Then, after ten seconds, you stop it by clearing the respective interval. Finally, to prevent PyScript from showing the timeout’s identifier, you assign the return value of setTimeout() to a placeholder variable denoted with an underscore (_), which is a standard convention in Python.

All right. These were the essential parts of the web browser interface that you could use in JavaScript, which are now available to you in Python, thanks to PyScript. Next up, you’ll have a chance to use some of the web browser’s capabilities to enhance your hands-on PyScript project.

Combining the Power of Python and JavaScript Libraries

One of the strengths of PyScript is the ability to combine existing libraries written in Python and JavaScript. Python has numerous fantastic data science libraries but not so many JavaScript equivalents. On the other hand, JavaScript has always naturally lent itself to building attractive user interfaces in the browser.

In this section, you’ll build a PyScript application that ties Python and JavaScript libraries together to make an interactive user interface in the browser. More specifically, you’ll simulate a sine wave interference of two slightly different frequencies, known as a beat in acoustics. By the end, you’ll have the following client-side application:

Sine Wave Interference

There are two sliders, which let you fine-tune the frequencies, and a <canvas> element depicting the plot of the waveform, which results from superimposing the two signals. Moving the sliders causes the plot to update in real-time.

You’ll perform the calculations in Python’s NumPy library, and you’ll draw the result using JavaScript’s open-source Chart.js library. It’s worth noting that Chart.js isn’t as fast as some of its paid competitors, but it’s free and fairly straightforward to use, so you’ll stick with it for now.

HTML and CSS

As the first step, you’ll need to scaffold your HTML document structure, style it with CSS, and include some necessary boilerplate code. Because this example is slightly more involved than the ones that you saw earlier in this tutorial, it makes sense to keep Python, JavaScript, and CSS code in separate files and link them in HTML. Save the following code in a new file named index.html:

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Sine Wave Interference</title>
    <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
    <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <link rel="stylesheet" href="theme.css" />
    <py-env>
      - numpy
      - paths:
        - src/waves.py
    </py-env>
</head>
<body>
  <fieldset>
    <legend>Frequency<sub>1</sub></legend>
    <input id="range1"
      type="range" min="440" max="442" step="0.1" value="440">
    <label for="range1">440</label> Hz
  </fieldset>
  <fieldset>
    <legend>Frequency<sub>2</sub></legend>
    <input id="range2"
      type="range" min="440" max="442" step="0.1" value="441">
    <label for="range2">441</label> Hz
  </fieldset>
  <canvas id="chart"></canvas>
  <py-script src="src/controls.py"></py-script>
  <script src="src/plotting.js"></script>
</body>
</html>

You include the usual PyScript files in your HTML header, the Chart.js library served from the jsDelivr CDN, and a custom CSS stylesheet that you’ll host locally. You also list NumPy and a custom helper module, which you built previously, as your dependencies in a <py-env> tag. At the bottom of the document, you source the core Python module and the plotting code in JavaScript.

Your custom CSS stylesheet ensures a bit of padding around the document’s body and gives the canvas a fixed size:

CSS
/* theme.css */

body {
  padding: 10px;
}
canvas {
  max-width: 800px;
  max-height: 400px;
}

Remember to place your custom style rules after the PyScript stylesheet in HTML to have an effect. Because the browser reads the document from top to bottom, whatever comes next might override the previous rules.

With the presentation layer out of the way, it’s time to turn your attention to the plotting code for a second.

JavaScript Code

To use Chart.js, you need to create a Chart instance and pass your data to it at some point. While you only need to make the chart once, you’ll be updating your data many times over, so you can define a callback function to reuse that logic:

JavaScript
// src/plotting.js

const chart = new Chart("chart", {
  type: "line",
  data: {
    datasets: [{
        borderColor: "#007cfb",
        backgroundColor: "#0062c5",
      }
    ]
  },
  options: {
    plugins: {
      legend: {display: false},
      tooltip: {enabled: false},
    },
    scales: {
      x: {display: false},
      y: {display: false}
    },
  }
})

function updateChart(xValues, yValues) {
  chart.data.labels = xValues
  chart.data.datasets[0].data = yValues
  chart.update()
}

The chart’s constructor function expects the id attribute of a <canvas> element where your plot will appear and a configuration object describing the look and feel of the chart. Your callback function also takes two arguments—that is, the x and y values of the data series to plot. These data points come from your Python code, which you’ll check out now.

Python Code

There are two files with Python source code in this application. One is a utility module with a helper function that can generate wave functions having the desired frequency, amplitude, and phase. At the same time, the other Python file is the controller layer of your application, which imports the former.

Here’s the utility module depicted for your convenience again:

Python
# src/waves.py

import numpy as np

def wave(frequency, amplitude=1, phase=0):
    def _wave(time):
        return amplitude * np.sin(2 * np.pi * frequency * time + phase)

    return _wave

While you’ve already used this function before, the controller module might need a few words of explanation, as it contains a fair amount of code:

Python
 1# src/controls.py
 2
 3import numpy as np
 4from pyodide import create_proxy, to_js
 5
 6from js import updateChart
 7from waves import wave
 8
 9range1 = document.querySelector("#range1")
10range2 = document.querySelector("#range2")
11
12sampling_frequency = 800
13seconds = 1.5
14time = np.linspace(0, seconds, int(seconds * sampling_frequency))
15
16def on_range_update(event):
17    label = event.currentTarget.nextElementSibling
18    label.innerText = event.currentTarget.value
19    plot_waveform()
20
21def plot_waveform():
22    frequency1 = float(range1.value)
23    frequency2 = float(range2.value)
24
25    waveform = wave(frequency1)(time) + wave(frequency2)(time)
26    updateChart(to_js(time), to_js(waveform))
27
28proxy = create_proxy(on_range_update)
29range1.addEventListener("input", proxy)
30range2.addEventListener("input", proxy)
31
32plot_waveform()

Here’s a breakdown of what this code does line by line:

  • Lines 3 and 4 import code from the required third-party libraries.
  • Lines 6 and 7 import your custom functions from Python and JavaScript utility modules that you linked in HTML before.
  • Lines 9 and 10 query the HTML document with CSS selectors to find the two slider elements and save them in variables for later use.
  • Lines 12 to 14 compute the x values of your time series, which has a duration of one and a half seconds sampled eight hundred times per second.
  • Lines 16 to 19 define a callback function responding to value changes of one of your sliders, which represent wave frequencies. The function updates the corresponding label in HTML and calls another function to recalculate the waveform and update the plot.
  • Lines 21 to 26 define a helper function, which reads the current frequency values from the sliders, generates new wave functions, and adds them together for the specified time duration. Next, the helper function converts the resulting x and y values to JavaScript proxies with Pyodide’s to_js(), and passes them to a callback defined in your JavaScript module.
  • Lines 28 to 30 wrap your on_range_update() event listener in a JavaScript proxy and register it as a callback in both sliders so that the web browser will call it when you change the sliders’ values.
  • Line 32 calls the plot_waveform() helper function to show the initial plot of the waveform when the page loads.

Alternatively, if you wish to use Matplotlib for plotting to eliminate JavaScript from your code, then you could create a figure and use PyScript’s Element.write() method like this:

Python
import matplotlib.pyplot as plt

fig, _ = plt.subplots()
# ...
Element("panel").write(fig)

The Element() class constructor takes the id attribute of an HTML element, which is usually a <div>.

Feel free to enhance this project—for example, by turning the generated acoustic wave into an audio stream and playing it in your browser. It has a peculiar sound! On the other hand, if you’re satisfied with the project in its current form, then you’ll learn how to share it for free with anyone in the world, even if they don’t have a Python interpreter installed on their computer.

Publishing Your PyScript Application on GitHub Pages

Because PyScript allows you to run code entirely in your client’s web browser, you don’t need a back-end server to have that code executed for you. Therefore, distributing PyScript applications boils down to hosting a bunch of static files for the browser to consume. GitHub Pages are a quick and straightforward way to turn any of your Git repositories into a website for free.

You’re going to reuse your code from the previous section, so before going any further, make sure that you have the following directory structure in place:

Text
sine-wave-interference/
│
├── src/
│   ├── controls.py
│   ├── plotting.js
│   └── waves.py
│
├── index.html
└── theme.css

To start using GitHub Pages, log in to your GitHub account and create a new public repository named sine-wave-interference, leaving all the default options. You don’t want GitHub to create any files for you at this stage, because they would conflict with the code you already have on your computer. Take note of your unique repository URL afterward. It should look something like this:

Text
git@github.com:your-username/sine-wave-interference.git

Now, open the terminal and change your working directory to the project root folder. Then, initialize a new local Git repository, make your first commit, and push the files to the remote repository on GitHub using your unique URL:

Shell
$ cd sine-wave-interference/
$ git init
$ git add .
$ git commit -m "Initial commit"
$ git remote add origin git@github.com:your-username/sine-wave-interference.git
$ git push -u origin master

You can now go to your GitHub repository settings and enable GitHub Pages by choosing the branch and folder to host. Since you only have the master branch, you want to select it from the dropdown menu. You should also leave the root folder / selected, and click the Save button to confirm. When you do, GitHub will need a few minutes until you can view your live website.

The public URL address of your repository will be similar to this:

Text
https://your-username.github.io/sine-wave-interference/

Congratulations! You can now share this URL with anyone who has a modern web browser, and they’ll be able to play with your PyScript application online. The next step for you after reaching this point would be to include the minified resources in your repository and update the relative paths in the HTML document to make the load time somewhat faster.

Contributing to PyScript

Getting involved in an open-source project can be intimidating. However, because PyScript is such a young framework, tapping into its source code and fiddling with it isn’t actually that difficult. All you need is a Git client, a recent version of Node.js, and the npm package manager.

Once you have those three tools configured, start by cloning the PyScript repository from GitHub and installing the required dependencies into a local node_modules/ folder:

Shell
$ git clone git@github.com:pyscript/pyscript.git
$ cd pyscript/pyscriptjs/
$ npm install --global rollup
$ npm install

You’ll run these commands only once because you won’t be adding any new dependencies to the project. Next, find the file named main.ts in the src/ subfolder, open it in your favorite code editor, and rename the custom <py-script> HTML tag associated with the PyScript class to, for instance, <real-python>:

File Changes (diff)
- const xPyScript = customElements.define('py-script', PyScript);
+ const xPyScript = customElements.define('real-python', PyScript);

Remember that custom tag names must contain a hyphen to differentiate them from regular ones. Then, in your terminal, change the current working directory to the pyscriptjs/ subfolder in the cloned project, and run the build command:

Shell
$ npm run build

This will produce a new pyscript.js file, among a few others, which you can host locally instead of linking to the official build on CDN. When you do, you’ll be able to embed Python code in your new shiny tag:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hello, World!</title>
  <link rel="stylesheet" href="/pyscript.css" />
  <script defer src="/pyscript.js"></script>
</head>
<body>
  <real-python>print("Hello, World!")</real-python>
</body>
</html>

Awesome! Now, you can fork this on GitHub and open a pull request. Just kidding! It was a toy example, but it nicely demonstrated a few basic steps of working with PyScript on a slightly lower level. Even if you don’t intend to make any contributions to PyScript, browsing its source code might give you a better understanding of its inner workings.

Conclusion

Now you have a pretty good idea of what PyScript is, how it works, and what it has to offer. You can mitigate some of its current shortcomings and even customize it to your liking. Additionally, you’ve seen several hands-on examples demonstrating the framework’s features and practical applications.

In this tutorial, you learned how to:

  • Build interactive front-end apps using Python and JavaScript
  • Run existing Python code in the web browser
  • Reuse the same code on the back-end and the front-end
  • Call Python functions from JavaScript and the other way around
  • Distribute Python programs with zero dependencies

PyScript is undoubtedly an exciting new technology that allows you to run Python code in the web browser thanks to Pyodide and WebAssembly. While there have been earlier attempts at this, PyScript is the first framework that runs a genuine CPython interpreter in the browser, making it possible to reuse existing Python programs with few to no modifications.

What do you think of PyScript? Will it ever truly replace JavaScript in the browser? Are you going to give it a try in your next project? Leave a comment below!

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Bartosz Zaczyński

Bartosz Zaczyński Bartosz Zaczyński

Bartosz is a bootcamp instructor, author, and polyglot programmer in love with Python. He helps his students get into software engineering by sharing over a decade of commercial experience in the IT industry.

» More about Bartosz

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Tutorial Categories: data-science data-viz front-end gui intermediate web-dev