What's New in Pyscript 2023.05.1

Published June 20, 2023

Tags: pyscript python pyodide javascript

PyScript has released version 2023.05.1 today! Between the big push to PyconUS 2023, the ensuing sprints and the following flurry of enthusiasm, it's been a busy couple of months. There's been some significant bonuses to functionality, in parallel with a backend overhaul that'll be dropping in a future version.

As always, you can see a Published Changelog for additional changes and bugfixes. But let's dive into the major changes in this PyScript release:


<script type="py"> == <py-script>

<script type="py"> is now a synonym for <py-script> , as are <script type="pyscript"> and <script type="py-script"> (#1396). But why have four tags, when we've been getting along fine with just one?

The truth is, we haven't quite been getting along. HTML Custom Elements (of which <py-script> is one) are treated just like any other displayable element - their contents are parsed as text, and that text is displayed on the screen, until and unless some styling specifies not to. This means that any characters that have special meaning to the HTML parser like < or > will be parsed like HTML tags, and our Python code becomes a mess. Currently, the only way around this is to make use of the special treatmean the browser affords the <script> tag, whose contents are left alone and are not displayed.

So if you are running into funky issues with < and > in your code, or code visible on your page in a way you don't want, converting your code to use <script type="py"> may do the trick.

The @when decorator

For building interactive and web-friendly pages in Python, one wants a Python way to hookup Python event handler functions to respond to Events that occur on the page. Thus, in addition to the existing py-* syntax, there's now a new way to hookup event handlers directly in one's python code: the @when decorator.

The @when decorator takes two arguments, both strings: the type of the event to listen for, and a css selector to match Elements to tie the event handler to. The decorated function can take 0 or 1 arguments; if the function takes zero arguments, it will simply be called when the matching event is dispatched. If it takes one argument, it will be called and passed the correpsonding Event object.

The combination of selectors and the ability to utilize the event object can create powerful interfaces with just a little bit of code. Consider the add_number example below, which uses only one decorated function to handle many buttons. In general, if you're using the @when decorator, consider how you can use containers, structure, and careful matching to minimize the number of decorators you need to apply.

The event listeners are added exactly once, when the @when decorator executes. That is, even if other elements are added to the DOM later that match the given selector, they will not have the event listener attached. Currently, there is no API for removing these event listeners. (Both are noted as desired features for the near future.)

You can decorate the same function with multiple instances of the @when decorator, to attach the same listener across multiple events or css selectors.

@When Decorator, Function takes no Arguments
from pyscript import when

@when('click', '#my_button')
def red():
    print("I love the color red")
@When Decorator, Function takes One Argument
from pyscript import when

@when('mouseenter', 'p.colorful')
def say_my_color(event):
@When Decorator, Many Buttons
from pyscript import when

value = 0

@when('click', 'div#controls button')
    global value
    addend = int(event.target.getAttribute("data-value"))
    new_value = value + addend
    print(f"{value} + {addend} = {new_value}")
    value = new_value

py-[event] Attributes now Dynamically Updated

Continuing the theme of an improved events API, py-[event] attributes, which were previously only assigned once at PyScript load time, are now updated dynamically whenever the attribute changes. This brings them closer to the behavior of the browser's native on[event]=... syntax, and allows for page interfaces to be dynamically hooked to Python events.

As a reminder of what that syntax is: any HTML element can be given an attribute of the form py-[event]="some code", where "event" is the type of some DOM Event. When the given even is observed on that element, the string of "some code" will be executed in the global namespace.

The term "executed" in the preceding paragraph is not incidental: the code is processed in a way that ultimately redounds to exec(). This means you can use multiple expressions separated by commas or newline characters if desired; while not necessarily the cleanest code practice, it does allow for inlining simple things like imports. See the examples below for some ideas.

<button py-click="print('Hello, world!')">Click to Say Hi</button>
<label for="yourname">What is your name?</label>
<input type="text"
 py-input="import js; elem = js.document.getElementById('yourname'); print(elem.value)">  

Dynamically Imported Pyodide

Early in PyScript's lifecycle, it needs to load the actual CPython interpreter/runtime that's going to be executing Python code in the Browser.. Currently, this is soely the Pyodide runtime. Pyodide is now imported into the current page via an import() statement instead of by adding a <script> tag to the page. This shouldn't impact end-user behavior, but if you were doing something like using the presence of that <script> tag to signal something, you'll have to find a new (better) method. (#1306)

No more 'Python Initialization Complete' Message

In its early stages, PyScript used the console log message "Python Initialization Complete" from Pyodide to signal when all of the user's scripts were run. In Pyodide 0.22, this message was removed, but PyScript added it into its own process to keep tests executing smoothly for the time being. That message has been removed from PyScript as well - if you relied on looking at the logs for that specific message for some reason, you will need to find a workaround. (#1373)


src attribute for <py-repl>

The <py-repl> tag now accepts a src attribute, whose value is a URL represented by a string. When changed or set, the text content from that URL is loaded as the code content of the REPL. This brings the behavior closer in line with the <py-script> tag, and allows for simpler pre-loading of REPL contents. (#1292). See also the added documentation (#1353)

The motivation for this feature (at least for the author of the PR) was being able to use a singular on-page REPL to present many different code samples. We've seen a number of folks interested in making their own Python code tutor site with PyScript, to whom this feature may also be useful. And just to say it again: the src attribute of a <py-repl> tag is a URL, which points to a resource containing the desired code, just like the src attribute of the <py-script>

<py-repl> attributes: output, output-mode, stderr

<py-repl> now accept three addition attributes: output, output-mode, stderr, all of which are strings. (#1106)

The output attribute specifies the ID of an element in the DOM where writes to stdout and stderr should also be printed, in addition to being written to the <py-terminal>(s). The stderr attribute behaves similarly, but only writes to sys.stderr will be written there (again, in addition to going to the <py-terminal>).

Setting output-mode == 'append' as the attribute of a <py-repl> means the output location for the REPL is not cleared before writing. This leads to decidedly un-notebook-like behavior, but it may be desirable for some demos or applications.

The motivation for this feature was to restore notebook-like behavior of a series of REPL cells on a page. It was enabled by the addition of two new plugin hooks for REPLs, which you can read about elsewhere in this post.

No More ID on py-repl run button

Previously, every REPL run-button had the same id of #runButton, which is a violation of the specified usage of the id attribute. No longer share this one id, and instead all share the class py-repl-run-button. If you need a programmatic way to cause of <py-repl> to execute, using a CSS selector or XPath to grab objects with this class (possibly inside a known parent) is the way to go. (#1296)


XTermjs Option

By default, a <py-terminal> is a very lightweight piece of content - just a <pre> tag with some css to style it. But many applications benefit from richer console output, and PyScript aims to be useful to those users as well.

To that end, Users can now add the xterm = True option to their <py-config> to turn the <py-terminal> into an xterm.js terminal, a fully in-browser terminal implemented in JavaScript. When loaded, the xterm is an output-only page element, but users can implement their own input functionality and extensions by targeting the <py-terminal>'s xterm attribute, which is a reference to the Terminal object itself (#1317)

Auto-Docked <py-terminal>

This release also changes the default placement of the <py-terminal>; rather than being stuck at the end of the DOM if the user doesn't specify a location, the terminal will appear "docked" at the bottom of the browser window. A new docked configuration option in <py-config> (default to 'docked') can be set to False to revert to the previous behavior. (#1284)


Add <py-repl> plugin hooks

The 2023.05.1 PyScript release adds two new Plugin Hooks: beforePyReplExec() and afterPyReplExec(). Plugin objects will have these methods called (if the exist) immediately before the execution of code by a <py-repl> and immediately after, allowing developers to inspect users' code before it executes, and respond to its results after the fact. (#1106)

The signaures of these plugin methods, in both Python and JavaScript, are:

* @param options.interpreter  The interpreter object that will be used to evaluated the Python source code
* @param options.src  {string} The Python source code to be evaluated
* @param options.outEl  The element that the result of the REPL evaluation will be output to.
* @param options.pyReplTag  The <py-repl> HTML tag the originated the evaluation
* @param options.result The result of evaluating the Python (if any)

beforePyReplExec(options: {
   interpreter: InterpreterClient;
   src: string;
   outEl: HTMLElement;
   pyReplTag: PyReplTag;

afterPyReplExec(options: {
   interpreter: InterpreterClient;
   src: string;
   outEl: HTMLElement;
   pyReplTag: PyReplTag;
   result: any;
def beforePyReplExec(self, interpreter: Interpreter, src: str, outEl: HTMLElement, pyReplTag: PyReplTag):

def afterPyReplExec(self, interpreter: Interpreter, src: str, outEl: HTMLElement, pyReplTag: PyReplTag, result: any):

As a brand-new plugin method that's been much requested on Community Discord and in the GitHub Discussions, we're excited to be releasing these. If there are extra parameters, features, or questions about these new methods, please let the team know!.

All Plugin Methods are awaited

In previous version of PyScript, plugin methods (both JavaScript and Python) were executed sequentially one after another, in the order the Plugins were originally added. As of this release, all JS plugins are executed at once via Promise.all([collect of js method for this plugin]), followed by all the Python plugin methods via Promise.all([collection of Py method for this plugin]) (#1467)

This shouldn't have too much impact on the functionality of plugins themselves, we think - but the team will be interested to hear whether anyone was indeed relying on plugins executing in a specific order. That wasn't a guaranteed feature of the interface, but having a deterministic execution order API for plugins is something we've bandied about a bit - if that would be useful, we'd love to hear about it.

Deprecation and Removals

pys-on* and py-on* attributes are removed

Existing from the very earliest days of py-script, the long-since-deprecated pys-onClick, py-onClick, py-onKeyDown and pys-onKeyDown HTML attributes have finally been removed from PyScript all together. They were superseded by the py-[event] syntax for hooking up event handlers. (#1361)

Py-Widget has Been Removed

The little known <py-register-widget> tag has been removed; this allowed for registering a named Python class as a custom HTML element. This ability is currently captured by the Plugins API (#1452)

py-mount is deprecated

This little-documented (and little-used) attribute had been available for "automatically" created proxies in Python for associated HTML elements. It's fallen out of step with the current recommended APIs, and since it wasn't much documented or recommended anyway, there were no qualms from the team about deprecating it. It will be removed in a future release.

Pyodide 0.23.2

PyScript now runs on Pyodide 0.23.2! As usual for a downstream project, PyScript basks in the glorious rays of Pyodide upstream, and the many improvements it has received.(#1347). While the Pyodide team wrote up an excellent post for the release of Pyodide 0.23, I do want to take a moment to highlight some of the larger and more exciting changes:

Python 3.11.2

Pyodide 0.23 is pinned to Python version 3.11.2, an upgrade from the 3.10.6 that had previously been bundled. With it come myriad improvements, including:

  • Improved error messages in Exceptions and Tracebacks (PEP 657)
  • Exception Groups (PEP 654)
  • Adding tomllib to the standard library (PEP 680)
  • The Faster CPython project has been making some strides in speeding up Python generally!
  • Many improvements to typing and the type system

Additionally, Python 3.11 is the first release of CPython to support Web Assembly as a Tier 3 Platform. The tiering system of supported platforms describes the level of build and issue support each platform can expect. Tier 1 includes x86 Mac, Windows, and Linux - the heavy hitters. Any issues that break these builds block a new release. Tier 3 requirements are much looser, including:

  • Must have a reliable buildbot. (i.e. the Tests Pass)
  • At least one core developer is signed up to support the platform.

What's more, issues in a Tier 3 Platform do not block a build, and there's no response SLA to failures. For more background on the considerable of Web Assembly-Emscripten (and WASI) as Tier 3 platforms, check out this discussion on the Python Discuss (some familiar PyScript names there) and the proposal and discussion of that change to the Steering Council.

New Packages

Many new packages have been pre-built by the Pyodide team to work with Pyodide, with their C/Rust/Fortran extensions pre-compiled, including: fastparquet, cramjam, pynacl, pyxel, mypy, multidict, yarl, idna, and cbor-diag .

A particularly fun one, in my personal opinion, is the pyxel game engine package. This retro-framework allows users to write games in pure Python, in the style of early-90s compturs (16-colors and 4 sounds at a time being the chief limitations). The Pyxel and Pyodide teams have both been working to make this engine work out of the box in the browser, and It's built on top of the SDL2 Support in Emscripten. From personal experience, the ability to run serverless web games right in the user's browser window, written in pure Python, is electric.

Efforts on Download Size

The Pyodide team is aware (as in the PyScript team) of the onus that download-size has on the viability of Python in the Browser, and has been making efforts to reduce their download size wherever possible. These efforts are, of course, offset by the continually growing nature of the Python standard library, so the overall download size hasn't changed much. But looked at another way, it's gained more functionality while staying about the same size, which isn't nothing.

One experimental effort includes an option to use a version of the Pyodide runtime that pre-compiles the Pyodide packages and the standard library to .pyc files. You can point your runtimes variable in <py-config> to https://cdn.jsdelivr.net/pyodide/v0.23.0/pyc/pyodide.js to give it a try! Note that, because the deployment doesn't include the standardlib source code, tracebacks and error messages will not look great, but for some applications this may be acceptable.

Removed Deprecated Object Nmaes

Many key functions of the Pyodide Python API, like create_proxy(), to_js(), and eval_code(), used to be accessibile directly from the pyodide root package, but were moved to individual submodules in version 0.21 and showed a deprecation warning if imported from root. Now, those functions and many, many others truly can only be accessed from their appropriate submodules (pyodide.ffi.create_proxy(), pyodide.ffi.to_js(), and pyodide.code.eval_code(), for example). (#3677)


Pull Request Template

PyScript now has a Pull Request template, to help contributors supply context and complete information with their PRs (#1279).

Typescript 5

PyScript is now built using TypeScript 5 (#1377). While we're not making use of many of it's powerful new features yet, we're glad to be using the latest release.

Community / Core

PyConUS 2023

A bunch of folks from the PyScript team attended PyCon in Salt Lake City in April, and what a delight it was! Between a tutorial session and three additional PyScript-centric talks, and even more PyScript adjacent presentations by team members, it was a roller coaster of a weekend, but a joyous one. You should really check out the full list of PyScript talks from this year. (Now with Video!)

I won't pretend to speak for PyScript as a project here, nor even the team, but just for myself, here were some key takeaways from my experience at PyCon:

  • People want to make things for the Web, and they want to write Python to do it. PyScript is hardly the only player in this space, with folks like Anvil and Pynecone both having great showings in their "write Python and we'll turn it into a front- and/or back-end" offerings.
  • The Web is a strange place for Pythonistas, the Browser in particular. Without a cozy command line, synchrnous file system, and threads, web limitations are like a foreign language to Python users.
  • Ease of deployment is a huge feature. Users love the ability to write some code and give it to/run it for someone else with minimal additional setup.

New Core Contributor: Andrea Giammarchi

The PyScript team is delighted to have Andrea Giammarchi as a new core contributor and Anaconda staff member. He has a long history of building web tools, working with Web Standards, and hacking together JavaScript solutions and polyfills for the betterment of the Web. Andrea brings with him a deep fluidity in JavaScript, which is fortifying PyScript's technical bedrock at a frankly astonishing pace. His Medium Blog is well worth a read and a follow as well. (#1450)


As a personal project, I soft-launched pyscript.recipes recently as a repository for simple strategies for working with PyScript and Pyodide. Many of the questions I see float through the Discord, Stack Overflow, or the official forum are of the same kind, so I put together a central location where answers could live and be accessible.

Check it out for PyScript tips, and if you have a recipe to contribute, please submit it!

What's Next? PyScript Next

As alluded to earlier, the PyScript team is in the midst of a massive overhaul of the PyScript codebase. The goal is to streamline the PyScript lifecycle, bring it more in line with web standards, and allow for faster and cleaner expansion of the PyScript with new features (and potentially new languages). Take a peak at the PyScript:next branch to check out the work that's been happening there (in the pyscript.core folder).

Since this work is proceeding forward at lightspeed, I'd hate to share any firm predictions of what's coming out next for PyScript, as it's very possible I could be wrong in the direction the bullet train is heading. But I'm excited by where it's been and where it lands.