概要
marimoからPyScriptを使って、ブラウザ上でPythonのコードを実行したい。
そんな用途のために、Pythonの非同期関数を渡すだけでPyScriptを実行し、結果をHTML要素として表示できるパッケージ「marimo-pys」を作りました。
marimo-pysとは
marimo-pysは、marimoのセルからPyScriptを実行するための小さなパッケージです。
run_pyscript() に async def で定義したPython関数を渡すと、関数のソースコードを取り出し、PyScriptで実行するHTMLを生成します。生成したHTMLはiframeに格納され、marimo.Html として返されます。
つまり、marimo側で定義した関数を、ブラウザ側の独立したPython実行環境で実行する仕組みです。
インストール
pipの場合:
pip install marimo marimo-pys
uvでプロジェクトに追加する場合:
uv add marimo marimo-pys
すでにmarimoを導入していれば、marimo-pys だけ追加すればOKです。
また、パッケージをインストールせず、GitHubの marimo_pys/__init__.py のコードをmarimoのセルにコピーして使うこともできます。
基本的な使い方
次のコードをmarimoのセルに記述します。
from marimo_pys import run_pyscript
async def hello(js, data):
element = js.document.createElement("p")
element.textContent = f"こんにちは、{data['name']}!"
js.document.body.appendChild(element)
run_pyscript(hello, data={"name": "PyScript"}, height="120px")
ブラウザ側で hello() が実行され、iframe内に「こんにちは、PyScript!」と表示されます。
ポイントは、通常のPython関数ではなく async def で定義することです。run_pyscript() は関数を次のような形で呼び出します。
await hello(js, data)
js はPyScript側のJavaScript連携用モジュール、data は run_pyscript() に指定したデータです。JavaScriptのDOM APIを利用して、HTML要素を生成・変更できます。
関数の戻り値を自動表示する仕組みではないため、画面への出力は上記のようにDOMを操作するなどして行います。
なぜasync関数を渡すだけで動くのか
内部では、おおむね次の手順を踏んでいます。
-
inspect.getsource()でPython関数のソースコードを取得する - ソースコードに、関数を呼び出すための処理を追加する
- コードをBase64にエンコードし、PyScript用のHTMLに埋め込む
- HTMLを
iframe srcdocに格納し、marimoのHTMLオブジェクトとして返す
実行するのは、marimoのPython環境ではなくiframe内のPyScriptです。
また、data に指定した辞書はJSONを経由して受け渡しています。
実装で苦労したところ:PyScriptのURL解決
当初は、PyScriptを読み込むHTMLを生成して、そのままiframeに渡せば動くと考えていました。
ところが、iframe srcdoc で生成したHTMLでは、PyScriptの内部処理がURLを解決できず、正常に起動しない問題がありました。
原因は、PyScriptが内部で利用するpolyscriptの相対URL解決処理です。
通常のWebページであれば location.href を基準に相対URLを解決できます。しかし、srcdoc 内では location.href が about:srcdoc になるため、その値を基準URLにするとURLの生成に失敗します。
そこで、JavaScriptの URL クラスにモンキーパッチを当てることにしました。
実装の要点を抜粋すると、次のようになります。
const NativeURL = window.URL;
class PatchedURL extends NativeURL {
constructor(url, base) {
const b = base == null ? '' : String(base);
if (b === '' || b.startsWith('about:')) {
base = 'https://pyscript.net/releases/2026.7.3/';
}
super(url, base);
}
}
window.URL = PatchedURL;
基準URLが空、または about: で始まる場合に、PyScriptのリリースURLへ置き換えています。
これをPyScriptの core.js を読み込む前に適用することで、問題となっていた相対URLの解決を回避しています。
なお、これはPyScript 2026.7.3で確認した回避策です。ブラウザ標準のURLクラスの挙動をiframe内で変更しているため、PyScriptの更新時には動作確認が必要です。
その他の機能
run_pyscript() には、表示や実行環境を調整するためのオプションも用意しています。
| 引数 | 用途 |
|---|---|
width, height
|
iframeの表示サイズ |
data |
Python関数に渡すデータ |
config |
PyScriptの設定 |
pys_type |
mpy、py、py-game の選択 |
pys_version |
使用するPyScriptのバージョン |
terminal |
ターミナル表示の有効化 |
add_script, add_module, add_css
|
JavaScriptやCSSの追加 |
body_style, iframe_style
|
CSSスタイルの指定 |
iframe_sandbox |
iframeのsandbox属性の指定 |
add_dangerous_html |
HTMLを直接追加 |
標準の pys_type は mpy(MicroPython)で、py を指定するとPyodideを使用できます。
例えば、Pyodideを使いたい場合は次のように指定します。
run_pyscript(hello, pys_type="py", data={"name": "Pyodide"})
使用上の注意点
関数のソースコードを取り出して、ブラウザ側の独立したPython環境で実行するため、marimo側の変数やインポート済みモジュールをそのまま参照することはできません。
必要な値は data で渡し、必要なPythonモジュールは関数の内部でインポートしてください。data に渡せるのはJSONに変換できる値です。
また、関数に付けたデコレータには対応していません。HTMLを直接追加する add_dangerous_html には、信頼できない内容を渡さないでください。
おわりに
marimo上でPythonの処理を書きつつ、その一部をPyScriptによってブラウザ側で実行できるようにしてみました。
run_pyscript() にasync関数を渡すだけなので、PyScript用のHTMLを毎回手書きする必要はありません。
実装ではpolyscriptのURL解決に少し苦労しましたが、JavaScriptのURLクラスをモンキーパッチすることで回避しています。
ソースコードはGitHubで公開しています。興味があれば使ってみてください。