It is a well known law of the modern internet: Frontend logic is written in JavaScript. While this has been served to us well for client-side scripting, JavaScript is not known for it's ease of use.
Fear no more—PyScript is here.
Many kids, learning developers, and a lot of backend engineers learn and use Python as their primary language. This, coupled with machine learning and datascience being dominated by Python, creates a demand for a Python-like frontend logic alternative, a demand for PyScript.
PyScript is a framework that allows Python code to run in the browser with near-native speed. This is accomplished with a toolchain of Pyodide-Emscripten-Wasm. Pyodide takes CPython, adds an interface between Python and Typescript/JavaScript, and using Emscripten compiles it down to Wasm. Wasm binaries executes at near-native speed and is supported by all major browsers.
With PyScript we can:
To use PyScript you just add these lines to your HTML header*:
<html> <head> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" /> <script defer src="https://pyscript.net/latest/script.js"></script> </head> </html>
Having done so, we are now empowered to utilize PyScript in our webpage.
PyScript defines several custom HTML tags:
This is where you type your Python code!
<py-script> print('Hello from py-script!') </py-script>
This is the most straight-forward of the tags, and its flexibility is vast, but it is best used in tandem with:
<py-config> allows you to specify config, dependencies from PyPI, and also from your own code:
<py-config> packages = [ "numpy", "pandas", "pytorch", "plusminus", ] paths = [ "my_code.py", ] </py-config>
Yes, numpy, pandas, matplotlib are all pre-compiled for Wasm, so we can circumvent the portability issues often experienced with data libraries implementing a lot of C code. And again, since it compiles to Wasm, these still run at near-native speed.
In addition to targeting your own code, you can import other files too:
<py-config> packages = [ "matplotlib", "pandas", "requests" ] paths = [ "volcano.csv", "my_code.py", "tl_logo_flat.png", ] </py-config>
import pandas as pd def transform_data(csv_file_data: pd.DataFrame) -> pd.DataFrame: # Transform it to a long format df = csv_file_data.unstack().reset_index() df.columns = ["X", "Y", "Z"]<span class="c1"># And transform the old column name in something numeric</span> <span class="n">df</span><span class="p">[</span><span class="s1">'X'</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">Categorical</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s1">'X'</span><span class="p">])</span> <span class="n">df</span><span class="p">[</span><span class="s1">'X'</span><span class="p">]</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s1">'X'</span><span class="p">]</span><span class="o">.</span><span class="n">cat</span><span class="o">.</span><span class="n">codes</span> <span class="k">return</span> <span class="n">df</span>
And then use them:
<py-script output="plot_display"> import pandas as pd from matplotlib import pyplot as plt from my_code import transform_data # Get the data # data sourced from https://github.com/holtzy/The-Python-Graph-Gallery/blob/master/static/data/volcano.csv data = pd.read_csv('volcano.csv') # Transform the data using your code df = transform_data(data) # Create the figure and add the TL logo plt.style.use('dark_background') fig = plt.figure() img = plt.imread("tl_logo_flat.png") imax = fig.add_axes([-.1, -.1, 1.2, 1.2]) imax.axis('off') imax.imshow(img) # Create the plot ax = fig.add_axes([0, 0, 1, 1.25], projection='3d', facecolor='none') ax.plot_trisurf(df['Y'], df['X'], df['Z'], cmap=plt.cm.jet, linewidth=0.01, alpha=.4) ax.axis('off') print('Here is your plot:') fig # Like print() this will display the plot. </py-script> <div id="plot_display" align="center"></div>
data = pd.read_csv('volcano.csv')
df = transform_data(data)
plt.style.use('dark_background') fig = plt.figure()
img = plt.imread("tl_logo_flat.png") imax = fig.add_axes([-.1, -.1, 1.2, 1.2]) imax.axis('off') imax.imshow(img)
ax = fig.add_axes([0, 0, 1, 1.25], projection='3d', facecolor='none') ax.plot_trisurf(df['Y'], df['X'], df['Z'], cmap=plt.cm.jet, linewidth=0.01, alpha=.4) ax.axis('off')
print('Here is your plot:') fig # Like print() this will display the plot:
Yep, you can pull your own code along for the ride and import it inside your <py-script>
tags. You can actually bundle files of any type and use them in your code.
But we aren't limited to server-side code. We can pass on this same power to the client-side user in the browser:
PyScript comes with a built-in repl tag:
<py-repl std-out="pyrepl-std-out" std-err="pyrepl-std-err"></py-repl> <div id=repl-output> <b>Standard Output:</b> <div class="hll" id="pyrepl-std-out"></div> <br/> <b>Error Messages:</b> <div class="hll" id="pyrepl-std-err"></div> </div>
If you type Python code in the repl above, and then hit shift+enter
or click the arrow to run it, you can see output as standard output or an error.
There are also the <py-button>
and <py-inputbox>
widgets that allow you to call Python code from click and keypress events respectively, and <py-box>
, a container object that can define how it's elements are arranged and displayed à la CSS Flexbox.
What does this mean, aside from being able to write Python instead of JavaScript on the frontend?
Despite warnings that PyScript is "very alpha" and changes should be expected, it has the potential to prove one of the most pivotal developments of the decade in frontend development, and web development generally.
Python has always had portability and GUI challenges. PyScript offers a solution to both, offering a Python-friendly extension of existing web technologies as well as providing Python wherever there is a browser. Furthermore, PyScript in the browser is a single target platform for Python developers to deliver native cross-platform applications, even offering the prospect of relatively trivial mobile development, by wrapping PyScript based web applications with mobile-native languages. A mere glance at frameworks like Electron betrays a glimpse of the potential.
For datascience and ML, this frees developers to ship their computation-heavy applications off servers and into client devices.
Got an application that runs on sensitive data that clients would rather keep on their machines? Ship the application with PyScript, and clients can leverage your application without their data ever leaving their machine.
Any code that can run on a client, but struggles because of installation/dependency issues, can be served client-side with PyScript. Numpy is infamous for being difficult to install, but it is now compiled for Wasm everywhere. Any such tools or code that can be compiled to Wasm can now be leveraged with PyScript, client-side.
Look to Peter Wang's demonstration at PyCon: motion-control married to software emulation married to GUI can be delivered in-browser.
Terminal Labs is ecstatic to have been a part of bringing PyScript to a browser near you already. Please reach out to us; we can help you bring your special sauce to the plate of your clients' browsers.
Disclaimer: instead of the original css pyscript styling this blog post includes a lightly altered version due to css style conflicts with the our website (and we have self-vendored pyscript.js):
<link rel="stylesheet" href="/static/css/tl-pyscript-2022-09-01.css" /> <script defer src="/static/js/pyscript-2022-09-01.js"></script>
This allowed our website to avoid some conflicts with our CSS and present the stark "pyscript style" confined within the bounds of the blog. Additionally, we removed the frozen loading screen and moved it to the wheel in the bottom left corner while this page loads.
This is the diff of our css from pyscript's, after accounting for inconsequential formatting.
diff /pyscript.net/releases/2022.09.1/pyscript.css /static/css/tl-pyscript-2022-09-01.css--- /pyscript.net/releases/2022.09.1/pyscript.css +++ /static/css/tl-pyscript-2022-09-01.css @@ -1,12 +1,19 @@ -:not(:defined) { - display: none; +#pyscript_loading_splash { + max-height: 12rem; + position: fixed; + bottom: 0; + left: 0; + background-color: #0000; + max-width: 10rem; +} + +#btnRun { + opacity: 100; }
-html { - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, - "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - line-height: 1.5; +.cm-content { + background-color: white; + color: black; }
.spinner::after { @@ -49,7 +56,6 @@ html { justify-content: center; align-items: center; color: white; - top: 0; bottom: 0; left: 0; right: 0; @@ -79,7 +85,7 @@ html { color: white; text-decoration: none; font-size: 200%; - top: 3.5%; + bottom: 3.5%; right: 5%; }
@@ -122,7 +128,7 @@ html {
.repl-play-button { opacity: 0; - bottom: 0.25rem; + bottom: 0.15rem; right: 0.25rem; position: absolute; padding: 0; @@ -146,12 +152,16 @@ html { display: inline-block; text-align: center; align-items: flex-start; - cursor: default; box-sizing: border-box; background-color: -internal-light-dark(rgb(239, 239, 239), rgb(59, 59, 59)); margin: 0em; padding: 1px 6px; - border: 0; + border-color: var(--tw-shadow); } + +button, +[role="button"] { + cursor: pointer; +}
We have intentionally left some of the formatting stark to suit the needs of a tutorial. We can partner with you to integrate pyscript to meet your needs as well.