Pyodideで改行を含むコードを実行する際のOSError解決方法

## はじめに

WebブラウザでPythonを動作させる強力なツールとしてPyodideが注目を集めています。Pyodideを使用することで、ブラウザ上で直接Pythonコードを実行できるため、インタラクティブな教育コンテンツやデータ分析ツールの開発に使われています。

最近では、LLM（大規模言語モデル）が生成したPythonコードを安全に実行するためのプラットフォームとしても注目されており、CohereのTerrariumなどのツールでもPyodideが採用されています。このような実用的なケースでは、出力処理の安定性が特に重要になってきます。

https://qiita.com/yonaka15/items/47e521c1bf0ddcbe4392

さて、改行を含むPythonコードを実行しようとすると、以下のようなエラーに遭遇しました：

```
OSError: [Errno 29] I/O error
```

この記事では、このエラーが発生する原因を解説し、解決方法を提供します。

## 問題が発生するケース

以下のようなコードを実行しようとすると、OSErrorが発生します：

```typescript
// 問題のあるコード例
pyodide.setStdout({
  write: (text: string) => outputCapture.capture(text)
});

// 複数行のPythonコードを実行しようとする
const code = `
print("Line 1")
print("Line 2")
print("Line 3")
`;

await pyodide.runPythonAsync(code);
```

このコードを実行すると、`OSError: [Errno 29] I/O error`というエラーメッセージが表示され、プログラムが正常に動作しません。

## エラーが発生する技術的背景

このエラーは、Pyodideにおける標準出力（stdout）の処理方法に起因しています。主な問題点は以下の通りです：

1. デフォルトの`write`モードでは、出力が断片的に処理される可能性があります。
2. 改行を含むテキストが複数のチャンクに分割されて処理されることがあります。
3. 出力バッファの管理が適切に行われないことがあります。

これらの要因により、特に複数行のコードを実行する際にエラーが発生しやすくなっています。

## 解決方法：batchedモードの活用

この問題は、標準出力の設定を`batched`モードに変更することで解決できます：

```typescript
pyodide.setStdout({
  batched: (text: string) => outputCapture.capture(text)
});
```

`batched`モードには以下のような利点があります：

1. テキストを一括で処理するため、出力の断片化を防ぎます
2. 行単位で完結した出力を受け取ることができます
3. 部分的な行や中途半端なテキストフラグメントを避けることができます

### 実装例：エラーハンドリングを含む完全なコード

以下に、エラーハンドリングやタイムアウト処理を含む完全な実装例を示します：

```typescript
class OutputCapture {
  private output: string[] = [];

  capture(text: string) {
    this.output.push(text);
  }

  getOutput(): string {
    return this.output.join("\n");
  }

  clear() {
    this.output = [];
  }
}

const executePython = async (code: string, timeout: number) => {
  try {
    if (!pyodide) {
      throw new Error("Pyodide not initialized");
    }

    const outputCapture = new OutputCapture();
    
    // batchedモードで標準出力を設定
    pyodide.setStdout({
      batched: (text: string) => outputCapture.capture(text)
    });

    console.error("Executing Python code:", code);

    // タイムアウト付きで実行
    const resultPromise = pyodide.runPythonAsync(code);
    const result = await Promise.race([
      resultPromise,
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error("Execution timeout")), timeout)
      ),
    ]);

    return {
      content: [
        {
          type: "text",
          text: outputCapture.getOutput()
            ? outputCapture.getOutput()
            : String(result),
        },
      ],
    };
  } catch (error) {
    return {
      isError: true,
      content: [
        {
          type: "text",
          text: `Error executing Python code: ${
            error instanceof Error ? error.message : String(error)
          }`,
        },
      ],
    };
  }
};
```

このコードには以下のような特徴があります：

1. 出力のキャプチャ用クラスの実装
2. タイムアウト処理の導入
3. エラーハンドリングの実装
4. `batched`モードによる安定した出力処理

### batchedモードの動作の仕組み

batchedモードは、Pythonコードからの出力を効率的に処理するために設計された特別な動作モードです。このモードでは、テキスト出力の処理に特化した2つの主要なイベントトリガーが存在します：

1. 改行文字（newline）の書き込み時
   - Pythonコードが改行を含む出力を生成したとき（例：`print()`関数の使用時）
   - この時点でbatchedハンドラーが呼び出され、改行までの内容が処理されます

2. 明示的なflush操作時
   - `sys.stdout.flush()`が呼び出されたとき
   - バッファ内の内容が即座に処理されます

このような仕組みにより、テキスト出力の安定した処理が実現され、エラーの発生を防ぐことができます。特に複数行のコードを実行する場合や、大量の出力を伴うプログラムを実行する際に、その効果を発揮します。

標準出力の設定例：
```typescript
pyodide.setStdout({
  batched: (text: string) => {
    // textには完全な行または flush された内容が渡されます
    console.log(`Output: ${text}`);
  }
});
```

このような処理フローにより、出力の安定性が確保され、エラーを防ぐことができます。


## まとめ

Pyodideで改行を含むコードを実行する際の`OSError`は、標準出力の設定を`batched`モードにすることで解決できます。

ちょっとしたことですが、お役に立てば幸いです。

## 参考情報

- [Pyodide公式ドキュメント - Redirecting standard streams](https://pyodide.org/en/stable/usage/streams.html)
- [Pyodide GitHub Issue - Stdout not ending in newline doesn't trigger stdout callback](https://github.com/pyodide/pyodide/issues/3112)
- [Pyodide FAQ](https://pyodide.org/en/stable/usage/faq.html)