はじめに
WebアプリでPythonを使おうとすると、通常は以下のような構成が必要になりますよね。
・バックエンドサーバーの構築(FlaskやFastAPI、Djangoなど)
・API経由でのフロントとの通信処理
しかし、「Pyodide(パイオダイド)」という技術を使えば、これらは一切不要になります!
💡 Pyodideとは?
サーバーを一切立てずに、ユーザーのブラウザ上(フロントエンド)だけでPythonを動かすことができる画期的な技術(WebAssemblyポート)です。
今回、実際に触ってみて「これはフロントエンド開発の幅が広がる!」と感動した3つの知見と、すぐに試せる最小構成のサンプルコードを紹介します。
💡 Pyodideから得られた3つの知見
1. ブラウザだけでPythonが動く (WebAssembly)
特別な環境構築やDockerなどは一切不要です。CDNからJavaScriptのライブラリを1つ読み込むだけで、ブラウザのメモリ上にPythonの実行エンジンが立ち上がります。
2. ブラウザ上で「pip」が使える (micropip)
これが個人的に一番の驚きでした。Pythonの強みは膨大な外部ライブラリですが、ブラウザ上であっても micropip を使えば、PyPI(公式パッケージリポジトリ)から動的にライブラリをダウンロードしてインストールできます。
3. JSとPythonで変数を直接やり取りできる
JavaScriptの変数をPython側に渡し、Pythonで処理した結果(戻り値)を再びJavaScriptで受け取ることができます。JSONへの変換(シリアライズ)などを意識せず、シームレスに連携できるのが非常に強力です。
🛠️ 最小構成サンプル:JSからPythonにテキストを渡してアスキーアート化する
上記の3つの知見をすべて盛り込んだ、コピペで動くHTML1枚のサンプルコードです。
Pythonの pyfiglet というライブラリをブラウザ上でインストールし、JSから渡した文字列をアスキーアートに変換します。
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pyodide 技術実証デモ (Vanilla JS)</title>
<!-- Tailwind CSSの読み込み -->
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-slate-100 flex flex-col items-center py-10 px-4 font-sans text-slate-800">
<div class="max-w-2xl w-full bg-white rounded-xl shadow-lg overflow-hidden border border-slate-200">
<!-- ヘッダー -->
<div class="bg-slate-800 p-6 text-white text-center">
<h1 class="text-2xl font-bold flex items-center justify-center gap-2">
<!-- ターミナルアイコン (SVG) -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
Pyodide 技術実証デモ
</h1>
<p class="text-slate-400 text-sm mt-2">
ブラウザでのPython実行・PIPインストール・JS連携の最小サンプル (Vanilla JS版)
</p>
</div>
<div class="p-6 space-y-4">
<!-- コントロール -->
<div class="flex justify-between items-center">
<p class="text-sm font-medium text-slate-600">
ステータス: <span id="status-text" class="text-amber-500">初期化中...</span>
</p>
<button
id="run-btn"
disabled
class="px-6 py-2 rounded-lg font-bold flex items-center gap-2 transition-colors bg-slate-200 text-slate-400 cursor-not-allowed"
>
<span id="btn-icon">
<!-- プレイアイコン (SVG) -->
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
</span>
<span id="btn-text">デモを実行する</span>
</button>
</div>
<!-- ターミナル風のログ表示エリア -->
<div id="log-container" class="bg-slate-900 rounded-lg p-4 h-96 overflow-y-auto font-mono text-sm shadow-inner text-slate-500 italic">
「デモを実行する」をクリックすると、ここに処理ログが表示されます。
</div>
</div>
</div>
<!-- JavaScript ロジック -->
<script>
// DOM要素の取得
const statusText = document.getElementById('status-text');
const runBtn = document.getElementById('run-btn');
const btnText = document.getElementById('btn-text');
const btnIcon = document.getElementById('btn-icon');
const logContainer = document.getElementById('log-container');
let isReady = false;
let isRunning = false;
// Pyodideのスクリプトを動的に読み込む(読み込み順序によるReferenceErrorを回避)
const pyodideScript = document.createElement('script');
pyodideScript.src = 'https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js';
pyodideScript.onload = () => {
isReady = true;
statusText.textContent = "準備完了";
statusText.className = "text-green-600 font-bold";
updateButtonState();
};
document.body.appendChild(pyodideScript);
// ボタンの見た目と状態を更新する関数
function updateButtonState() {
if (!isReady || isRunning) {
runBtn.disabled = true;
runBtn.className = "px-6 py-2 rounded-lg font-bold flex items-center gap-2 transition-colors bg-slate-200 text-slate-400 cursor-not-allowed";
} else {
runBtn.disabled = false;
runBtn.className = "px-6 py-2 rounded-lg font-bold flex items-center gap-2 transition-colors bg-indigo-600 hover:bg-indigo-700 text-white shadow-md cursor-pointer";
}
if (isRunning) {
btnIcon.style.display = 'none';
btnText.textContent = "処理中...";
} else {
btnIcon.style.display = 'inline-block';
btnText.textContent = "デモを実行する";
}
}
// ログ画面にテキストを追加する関数
function addLog(msg) {
// 初回のプレースホルダーテキストを削除
if (logContainer.classList.contains('text-slate-500')) {
logContainer.innerHTML = '';
logContainer.className = "bg-slate-900 rounded-lg p-4 h-96 overflow-y-auto font-mono text-sm shadow-inner";
}
const pre = document.createElement('pre');
const isError = msg.includes('❌');
pre.className = `whitespace-pre-wrap leading-relaxed ${isError ? 'text-red-400' : 'text-green-400'}`;
pre.textContent = msg;
logContainer.appendChild(pre);
// 自動で一番下までスクロール
logContainer.scrollTop = logContainer.scrollHeight;
}
// デモのメイン処理
runBtn.addEventListener('click', async () => {
if (!isReady || isRunning) return;
isRunning = true;
updateButtonState();
// ログをクリアして開始
logContainer.innerHTML = '';
logContainer.className = "bg-slate-900 rounded-lg p-4 h-96 overflow-y-auto font-mono text-sm shadow-inner";
addLog("▶ デモ開始...");
try {
// --------------------------------------------------------
// 知見①: ブラウザだけでPythonを起動する
// --------------------------------------------------------
addLog("⏳ [1/4] Python環境をブラウザ上に構築しています...");
const pyodide = await window.loadPyodide();
addLog("✅ Python環境の起動完了!");
// --------------------------------------------------------
// 知見②: PIPを使って外部パッケージをインストールする
// --------------------------------------------------------
addLog("📦 [2/4] micropip(ブラウザ版pip)で 'pyfiglet' をインストール中...");
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
// ここで実際のPyPIからパッケージを取得しています!
await micropip.install("pyfiglet");
addLog("✅ インストール完了!");
// --------------------------------------------------------
// 知見③: JavaScriptからPythonへのデータ受け渡し
// --------------------------------------------------------
const jsMessage = "Hello Pyodide!";
addLog(`📤 [3/4] JSからPythonへ変数を渡します: "${jsMessage}"`);
pyodide.globals.set('js_message', jsMessage);
// --------------------------------------------------------
// 知見④: Pythonコードの実行と、結果のJSへの返却
// --------------------------------------------------------
addLog("⚙️ [4/4] Pythonスクリプトを実行します...");
const pythonCode = `
import pyfiglet
# JSから受け取った変数(js_message)を使ってアスキーアートを生成
art = pyfiglet.figlet_format(js_message)
art # 最後に評価された値が、JavaScriptに返却されます
`;
const result = await pyodide.runPythonAsync(pythonCode);
addLog("📥 Pythonからの返却値を受け取りました!👇\n");
addLog(result);
} catch (error) {
addLog(`❌ エラーが発生しました: ${error.message}`);
} finally {
isRunning = false;
updateButtonState();
addLog("\n⏹ デモ終了");
}
});
</script>
</body>
</html>
HTMLコードから、Pythonとして実行されている部分のみを再掲
import pyfiglet
# JSから受け取った変数(js_message)を使ってアスキーアートを生成
art = pyfiglet.figlet_format(js_message)
art # 最後に評価された値が、JavaScriptに返却されます
最後に art とだけ書かれている理由は、Pyodide(パイオダイド)特有の「戻り値の返し方のルール」にあります。
Pyodideの runPythonAsync() という機能でPythonのコードを実行した場合、「一番最後に評価された値(式)」が、自動的にJavaScript側への戻り値として渡されるという仕様になっています。
これには以下のようないくつかの理由と背景があります。
-
return は使えない
このPythonコードは関数(def ...:)の中ではないため、return art と書くとPythonの文法エラーになってしまいます。 -
print() では値が渡せない
print(art) と書いた場合、文字通り「出力(表示)」されてしまうだけで、JavaScript側の変数(今回のコードでいう result)には何も値が返ってきません。
💻 実行結果のイメージ
ボタンを押すと、バックエンドと通信することなく、以下のようなアスキーアートがブラウザ上で生成されます。
🚀 どんな時に役立つか?(ユースケース)
このアプローチは、「サーバーにデータを送りたくない」「バックエンドのインフラ管理をしたくない」というケースで絶大な威力を発揮します。
セキュアなファイル処理ツール
セキュアなファイル処理ツール等、PDFのパスワード設定や画像のリサイズなど。データがサーバーに送信されないため、情報漏洩のリスクがありません。
データサイエンス系のダッシュボード
PandasやNumPyなども動くため、ユーザーのブラウザの計算リソースを使ってデータ分析やグラフ描画が可能です。
教育用ツール・サンドボックス
ユーザーが書いたPythonコードをその場で安全に実行・評価する環境が簡単に作れます。
🎯 まとめ
JavaScriptだけでは実装が難しい複雑な処理も、Pyodideを使えば「フロントエンドの手軽さ」と「Pythonの強力なエコシステム」をいいとこ取りできます。
ちょっとしたツール作りに非常に便利なので、ぜひ皆さんもブラウザ上でPythonを動かしてみてください!
