PythonをインストールしていないPCでも、Pyodideを使えばWebAssembly版のPythonをブラウザ上で実行できます。本記事ではWindows 11で3つのファイルを作り、Pythonコードを入力して実行できるミニ実行環境を完成させます。Pyodideの仕組みを先に知りたい方は、「Pyodideとは?Pythonをブラウザで動かす仕組みと使い方」をご覧ください。
公式の最小例はloadPyodide()で初期化してrunPython()を呼ぶだけですが、実際に利用者が操作する画面では、初期化状態・実行結果・エラー表示・二重操作防止も必要です。今回はそこまで含めて実装します。
Contents
この記事で作るもの
- ブラウザ上でPythonコードを入力できるエディター
- Pyodideの初期化状態とローディング表示
- Pythonコードを実行するボタン
print()などの標準出力を表示する領域- 構文エラーや実行時例外を表示する領域
- 初期化中・実行中のボタン無効化と連打防止
完成後は、次のようなPythonコードを入力してブラウザだけで実行できます。
name = "Pyodide"
for i in range(3):
print(f"{i + 1}: Hello, {name}!")
sum(range(1, 11))
Windows 11でプロジェクトを作成する
任意の場所にpyodide-runnerフォルダーを作成し、Visual Studio Codeなどのエディターで開きます。フォルダーの中に次の3ファイルを用意してください。
pyodide-runner/
├─ index.html
├─ style.css
└─ app.js
PowerShellで作る場合は、次のコマンドでも準備できます。
mkdir pyodide-runner
cd pyodide-runner
New-Item index.html, style.css, app.js -ItemType File
HTML、CSS、JavaScriptの役割
| ファイル | 役割 |
|---|---|
index.html | 入力欄、実行ボタン、状態表示、結果表示を配置し、PyodideをCDNから読み込む |
style.css | 画面のレイアウト、ローディングアニメーション、状態別の色を設定する |
app.js | Pyodideの初期化、Python実行、標準出力・例外の表示、ボタン制御を担当する |
3つに分けることで、画面構造・見た目・処理をそれぞれ修正しやすくなります。ビルドツールやnpmは使いません。
index.htmlを作成する
index.htmlに次の内容を貼り付けます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Browser Python Runner</title>
<link rel="stylesheet" href="style.css">
<script
src="https://cdn.jsdelivr.net/pyodide/v314.0.5/full/pyodide.js"
defer
></script>
<script src="app.js" defer></script>
</head>
<body>
<main class="app">
<header class="app__header">
<p class="eyebrow">Pyodide Playground</p>
<h1>Browser Python Runner</h1>
<p>入力したPythonコードを、このブラウザ内で実行します。</p>
</header>
<div id="status" class="status" data-state="loading" role="status" aria-live="polite">
<span class="spinner" aria-hidden="true"></span>
<span id="statusText">Pyodideを初期化しています…</span>
</div>
<label for="codeInput">Pythonコード</label>
<textarea id="codeInput" spellcheck="false">name = "Pyodide"
for i in range(3):
print(f"{i + 1}: Hello, {name}!")
sum(range(1, 11))</textarea>
<button id="runButton" type="button" disabled>Pythonを実行</button>
<section class="result">
<h2>実行結果</h2>
<pre id="output" aria-live="polite">初期化が完了すると実行できます。</pre>
</section>
</main>
</body>
</html>
CDNからPyodideを読み込む
次のscriptタグがPyodide本体をCDNから読み込む部分です。本記事では確認時点の安定版であるv314.0.5を明示しています。
<script
src="https://cdn.jsdelivr.net/pyodide/v314.0.5/full/pyodide.js"
defer
></script>
バージョン番号を固定しておくと、CDN側の更新で突然動作が変わるリスクを抑えられます。新しいプロジェクトで使うときは、Pyodide公式ドキュメントで最新の安定版を確認してください。
style.cssを作成する
style.cssに次の内容を貼り付けます。状態表示のdata-state属性を使い、初期化中・準備完了・実行中・エラーで色を切り替えます。
:root {
color-scheme: dark;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
background: #07111f;
color: #e8eef8;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
padding: 32px 16px;
background:
radial-gradient(circle at top left, #193b67 0, transparent 35%),
#07111f;
}
.app {
width: min(900px, 100%);
margin: 0 auto;
padding: clamp(20px, 4vw, 40px);
border: 1px solid #29405f;
border-radius: 20px;
background: rgba(9, 24, 42, 0.92);
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.35);
}
.app__header h1 {
margin: 4px 0 8px;
font-size: clamp(2rem, 6vw, 3.5rem);
}
.eyebrow {
margin: 0;
color: #77bdfb;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
label,
.result h2 {
display: block;
margin: 24px 0 8px;
font-size: 1rem;
font-weight: 700;
}
textarea,
#output {
width: 100%;
border: 1px solid #385275;
border-radius: 12px;
background: #030a13;
color: #dcecff;
font: 15px/1.7 Consolas, "Courier New", monospace;
}
textarea {
min-height: 280px;
padding: 16px;
resize: vertical;
}
#output {
min-height: 150px;
max-height: 360px;
margin: 0;
padding: 16px;
overflow: auto;
white-space: pre-wrap;
}
#output.error {
border-color: #ff7b8a;
color: #ffd2d8;
}
button {
width: 100%;
margin-top: 16px;
padding: 14px 20px;
border: 0;
border-radius: 12px;
background: #2d8cff;
color: #fff;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
}
button:hover:not(:disabled) {
background: #1476e8;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.status {
display: flex;
align-items: center;
gap: 10px;
margin-top: 24px;
padding: 12px 14px;
border: 1px solid #385275;
border-radius: 12px;
background: #10243e;
}
.status[data-state="ready"] {
border-color: #2fbf8f;
background: #0e392f;
}
.status[data-state="error"] {
border-color: #ff7b8a;
background: #421d27;
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.status[data-state="ready"] .spinner,
.status[data-state="error"] .spinner {
display: none;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
app.jsを作成する
app.jsには初期化と実行処理をまとめます。次のコードをそのまま貼り付けてください。
const codeInput = document.getElementById("codeInput");
const runButton = document.getElementById("runButton");
const output = document.getElementById("output");
const status = document.getElementById("status");
const statusText = document.getElementById("statusText");
let pyodide = null;
let isRunning = false;
function setStatus(message, state) {
statusText.textContent = message;
status.dataset.state = state;
}
function clearOutput() {
output.textContent = "";
output.classList.remove("error");
}
function appendOutput(message, type = "stdout") {
const text = String(message).replace(/\n?$/, "\n");
output.textContent += text;
if (type === "stderr") {
output.classList.add("error");
}
output.scrollTop = output.scrollHeight;
}
async function initializePyodide() {
setStatus("Pyodideを初期化しています…", "loading");
runButton.disabled = true;
try {
pyodide = await loadPyodide();
pyodide.setStdout({
batched: (message) => appendOutput(message, "stdout"),
});
pyodide.setStderr({
batched: (message) => appendOutput(message, "stderr"),
});
clearOutput();
output.textContent = "準備ができました。Pythonコードを実行してください。";
setStatus("準備完了", "ready");
runButton.disabled = false;
} catch (error) {
clearOutput();
appendOutput(`初期化に失敗しました: ${error.message ?? error}`, "stderr");
setStatus("初期化エラー", "error");
}
}
async function executePython() {
if (!pyodide || isRunning) {
return;
}
isRunning = true;
runButton.disabled = true;
clearOutput();
setStatus("Pythonを実行しています…", "running");
let result;
try {
result = await pyodide.runPythonAsync(codeInput.value, {
filename: "editor.py",
});
if (result !== undefined && result !== null) {
appendOutput(result);
}
setStatus("実行完了", "ready");
} catch (error) {
appendOutput(error.message ?? error, "stderr");
setStatus("実行エラー", "error");
} finally {
if (result && typeof result.destroy === "function") {
result.destroy();
}
isRunning = false;
runButton.disabled = !pyodide;
}
}
runButton.addEventListener("click", executePython);
initializePyodide();
loadPyodide()の使い方
pyodide.jsの読み込みが完了すると、グローバル関数loadPyodide()を呼び出せます。この関数は非同期でPyodide本体やWebAssemblyを読み込み、初期化済みのAPIを返します。
const pyodide = await loadPyodide();
初回は数MB以上のファイルを取得するため、回線やPC性能によって数秒かかります。そこで、初期化中はステータスとスピナーを表示し、実行ボタンを無効にしています。
runPython()とrunPythonAsync()の違い
| API | 特徴 | 向いている用途 |
|---|---|---|
runPython() | 同期的に実行し、その場で結果を返す。Python側のトップレベルawaitは使えない | 短い計算、単純な式、同期処理だけのコード |
runPythonAsync() | Promiseを返し、トップレベルawaitを含むPythonコードも実行できる | UIからの実行、非同期処理を含むコード、将来の機能拡張 |
今回は実行前後でボタンや状態表示を切り替えやすく、将来Python側でawaitを使えるようにrunPythonAsync()を採用しました。
注意: runPythonAsync()を使っても、CPU負荷の高いPython処理が自動で別スレッドに移るわけではありません。重い計算を実行すると画面操作が固まることがあります。本格的な実行環境では、PyodideをWeb Worker内で動かす構成を検討してください。
Pythonの標準出力を画面に表示する
Pythonのprint()は標準出力へ書き込みます。PyodideのsetStdout()にbatchedコールバックを指定すると、出力された行をJavaScriptで受け取れます。
pyodide.setStdout({
batched: (message) => appendOutput(message, "stdout"),
});
同様にsetStderr()も設定しているため、Pythonが標準エラー出力へ書き込んだ内容も結果欄へ表示できます。
Python実行時の例外を画面に表示する
構文エラー、ZeroDivisionError、NameErrorなどが発生すると、runPythonAsync()が返すPromiseは失敗します。try...catchで捕捉し、例外メッセージを出力欄に表示します。
try {
const result = await pyodide.runPythonAsync(codeInput.value, {
filename: "editor.py",
});
} catch (error) {
appendOutput(error.message ?? error, "stderr");
setStatus("実行エラー", "error");
}
filename: "editor.py"を渡しているため、トレースバック上でも入力コードをファイルとして識別しやすくなります。動作確認では、入力欄にprint(10 / 0)と書いて実行してみてください。
実行中の二重操作を防止する
実行開始時にisRunningをtrueにし、ボタンも無効化します。連打や別の経路から関数が再度呼ばれても、冒頭の条件で処理を終了できます。
if (!pyodide || isRunning) {
return;
}
isRunning = true;
runButton.disabled = true;
finallyで必ず状態を元に戻すため、正常終了でも例外発生でもボタン制御が破綻しません。
ローカルWebサーバーで起動する
index.htmlをダブルクリックしてfile://で開くより、開発中はhttp://localhostのローカルWebサーバー経由で開くのがおすすめです。ブラウザのセキュリティ制約、相対パス、外部ファイル取得、将来追加するデータファイルなどを、本番のWebサイトに近い条件で確認できるためです。
PCにPythonがインストールされている場合は、PowerShellでプロジェクトフォルダーへ移動し、次を実行します。
py -m http.server 8000
pyコマンドが使えない場合は、python -m http.server 8000も試せます。その後、ChromeまたはEdgeでhttp://localhost:8000を開いてください。PCにPythonを入れたくない場合は、Visual Studio CodeのLive Server拡張機能を使う方法もあります。
動作確認
- ページを開くと「Pyodideを初期化しています…」とスピナーが表示される
- 初期化が終わると「準備完了」に変わり、実行ボタンが有効になる
- サンプルコードを実行すると
print()の3行と最後の式の結果55が表示される print(10 / 0)を実行すると例外が赤い結果欄に表示される- 実行中はボタンが無効になり、連打しても二重実行されない
うまく動かないときの確認ポイント
loadPyodide is not defined: PyodideのCDN URL、scriptタグの順番、deferの有無を確認します。- 初期化が終わらない: 開発者ツールのNetworkとConsoleを確認し、CDNへのアクセスがブロックされていないか調べます。
- 外部パッケージをimportできない: 標準ライブラリ以外は別途読み込みが必要です。
loadPackagesFromImports()やmicropipを利用します。 - 画面が長時間固まる: 重いPython処理がメインスレッドを占有しています。Web Worker化を検討します。
まとめ
Pyodideを使うと、サーバーへPythonコードを送らず、ブラウザ内でPython実行環境を作れます。最小構成から一歩進めるときは、loadPyodide()の待ち時間、標準出力・標準エラー出力、例外処理、ボタンの無効化をセットで設計するのがポイントです。
今回の3ファイルを土台に、コード履歴、サンプル選択、外部パッケージの読み込み、Web Workerによる実行などを追加すれば、より本格的なブラウザPython環境へ発展させられます。

コメント