Python 3.15のopen()既定はUTF-8かロケールか

本記事はアフィリエイトリンクを含みます。Amazon のアソシエイトとして、TechAthletes は適格販売により収入を得ています。価格・割引・在庫は変動するため、最新の条件は各公式サイトでご確認ください。

何が困るのか:encoding を書かずに open() した

Linux や macOS で書いたスクリプトを Windows に持っていくと、open("data.json") のように encoding を省略した箇所で UnicodeError や文字化けが出ることがある。PEP 686 はこの状況を次のように説明している。

Additionally, many Python developers using Unix forget that the default encoding is platform dependent. They omit to specify encoding="utf-8" when they read text files encoded in UTF-8 (e.g. JSON, TOML, Markdown, and Python source files). Inconsistent default encoding causes many bugs.

出典: https://peps.python.org/pep-0686/ (取得日 2026-10-15)

つまり「既定の文字コードは環境によって違う」という前提を忘れたまま書いたコードが、別の環境で壊れる。この記事では、その「既定」が公式にどう定義されていて、Python 3.15 で何が変わるのかを公式文書だけで追う。

公式はどう書いているか:PEP 540 と PEP 686

UTF-8 モードそのものは Python 3.7 の PEP 540 で入った。要旨は、ロケールを無視して UTF-8 を使い、stdin と stdout のエラーハンドラを surrogateescape に変える、というものだ。ただし導入時点では既定で無効で、POSIX ロケール(いわゆる C ロケール)のときだけ自動で有効になる設計だった。

This mode is off by default, but is automatically activated when using the "POSIX" locale. Add the -X utf8 command line option and PYTHONUTF8 environment variable to control UTF-8 Mode.

出典: https://peps.python.org/pep-0540/ (取得日 2026-10-15)

これを「既定で有効」に反転させるのが PEP 686 で、対象の版は 3.15 と明記されている。

Python will enable UTF-8 mode by default from Python 3.15. Users can still disable UTF-8 mode by setting PYTHONUTF8=0 or -X utf8=0.

出典: https://peps.python.org/pep-0686/ (取得日 2026-10-15)

PEP 686 は影響範囲についても書いている。多くの Unix 系は UTF-8 ロケールを使っており、C や POSIX ロケールのときは Python が UTF-8 モードを有効にするので、この変更が主に効くのは Windows 利用者だという。また、プログラムが既定の文字コードに依存している場合、この変更は UnicodeError や文字化け、さらには気づかないデータ破損を起こしうるので、大きく告知すべきだとも書かれている。

ロケールのほうの文字コードを知りたい場合のために locale.getencoding() が 3.11 で追加された。locale.getpreferredencoding(False) は UTF-8 モードの影響を受けるが、locale.getencoding() は UTF-8 モードを無視してロケールの文字コードを返す、と PEP 686 は説明している。

比べた表:既定と UTF-8 モードで何が変わるか

PEP 540 の付録にある表をそのまま引く。左が従来の既定、右が UTF-8 モード(または POSIX ロケール)のときの「文字コード/エラーハンドラ」である。

Function Default UTF-8 Mode or POSIX locale
open() locale/strict UTF-8/strict
os.fsdecode(), os.fsencode() locale/surrogateescape UTF-8/surrogateescape
sys.stdin, sys.stdout locale/strict UTF-8/surrogateescape
sys.stderr locale/backslashreplace UTF-8/backslashreplace

出典: https://peps.python.org/pep-0540/ (取得日 2026-10-15)

注目したいのは open() の行だ。文字コードは UTF-8 に変わるが、エラーハンドラは strict のままである。PEP 540 はその理由として、ファイルは正しく符号化されていることが期待されるので、JPEG をテキストモードで開いたような誤りは早く失敗させるべきだ、と書いている。標準入出力だけが surrogateescape になるのは、Unix のコマンドラインツールと同じく「復号できないバイトを素通しする」ためだ。

Windows については別の表がある。従来の既定では open() が mbcs/strict、sys.stdin と sys.stdout は UTF-8/surrogateescape だが、UTF-8 モードでは open() も UTF-8/strict になる。PEP 540 は「stdin や stdout がパイプにリダイレクトされていると既定では mbcs を使うが、UTF-8 モードでは常に UTF-8 を使う」とも書いている。Windows には POSIX ロケールが無く、ANSI コードページがロケールの文字コードとして使われる、という注記もある。

版をまたいだ既定の違いを一枚にまとめると次のようになる。

版 UTF-8 モードの既定 根拠
3.6 モード自体が無く、ロケールの文字コードを使う PEP 540
3.7 以降 既定は無効。POSIX ロケールのときだけ自動で有効 PEP 540
3.15 以降 既定で有効。PYTHONUTF8=0 か -X utf8=0 で無効化できる PEP 686

どちらを使うか

PEP 686 の立場ははっきりしていて、新しい利用者は文字コードを最初の年に学ぶ必要がなくなる、UTF-8 以外のテキストを扱うときに学べばよい、としている。したがって新しく書くコードで迷う余地はあまりない。

問題は既存のコードだ。PEP 686 は「後方互換の問題を直す手引き」として次の順番を示している。

  1. UTF-8 モードを無効にする。
  2. EncodingWarning(PEP 597)を使って、UTF-8 モードが影響する箇所をすべて見つける。
  3. encoding を省略している箇所は encoding="utf-8" か encoding="locale" に書き換える。locale.getpreferredencoding() を使っている箇所は "utf-8" か locale.getencoding() に置き換えることを検討する。
  4. UTF-8 モードを有効にしてアプリケーションを試験する。

要するに「どちらを使うか」を既定に任せるのをやめ、ファイルごとに意図を書くのが答えになる。UTF-8 で書かれたデータなら encoding="utf-8"、利用者のロケールに合わせたいなら encoding="locale" を明示する。

なお PEP 686 は、既定の文字コードを使うこと自体を DeprecationWarning で非推奨にする案を却下している。ASCII だけを読み書きする場面が多く、警告を大量に出すと利用者が警告を無視するようになる、というのが理由だ。パイプの文字コードに PYTHONIOENCODING を使う案も、既定を複雑にするうえ後方互換でもないとして却下されている。先行例として、Ruby は 3.0 で Windows の既定の external_encoding を UTF-8 にし、Java は JDK 18 で既定のテキスト文字コードを UTF-8 にしたが、どちらにも PEP 597 の EncodingWarning のような警告は無い、と PEP 686 は比較している。

落とし穴:getpreferredencoding と encoding="locale"

locale.getpreferredencoding() を「ロケールの文字コードを返す関数」だと思っていると、UTF-8 モードで足を取られる。PEP 540 は UTF-8 モードの効果を列挙する中で、この関数について次のように書いている。

locale.getpreferredencoding() returns UTF-8; its do_setlocale argument, and the locale encoding, are ignored.

出典: https://peps.python.org/pep-0540/ (取得日 2026-10-15)

つまり UTF-8 モードが有効な環境では、この関数はロケールを見ていない。3.15 以降は既定でモードが有効になるので、何も指定しなければこの関数は UTF-8 を返すことになる。ロケールのほうが本当に欲しい場合の答えは PEP 686 に書かれている。

Since UTF-8 mode affects locale.getpreferredencoding(False), we need an API to get locale encoding regardless of UTF-8 mode. locale.getencoding() will be added for this purpose. It returns locale encoding too, but ignores UTF-8 mode.

出典: https://peps.python.org/pep-0686/ (取得日 2026-10-15)

PEP 686 はさらに、warn_default_encoding オプションが指定されているときは locale.getpreferredencoding() も open() と同様に EncodingWarning を出す、としている。既定の文字コードに頼っている箇所を探すとき、open() だけでなくこの関数の呼び出しも警告の対象になるということだ。

もう一つは encoding="locale" の挙動だ。PEP 686 によると、PEP 597 で追加されたこのオプションは、UTF-8 モードでもロケールの文字コードを使うべきところ、当初は UTF-8 モードだと UTF-8 を使ってしまう不整合があった。これは 3.11 で修正されたと書かれている。3.11 より前の版でこのオプションに頼っている場合は、公式の記述どおりには動かない可能性があることになる。

環境変数の優先順位も PEP 540 に明記されている。標準ストリームについては PYTHONIOENCODING が UTF-8 モードより優先され、Windows では PYTHONLEGACYWINDOWSFSENCODING が UTF-8 モードより優先される。モードを有効にしたのに想定と違う文字コードになるときは、この二つを先に疑うとよい。

最後に、UTF-8 モードとロケール強制(PEP 538)の関係も押さえておきたい。PEP 540 によると、POSIX ロケールではロケール強制と UTF-8 モードの両方が有効になり、ロケール強制が効いていれば UTF-8 モードを有効にしても追加の効果は無い。違いは効く範囲で、UTF-8 モードの変更は Python のコードにだけ効くが、ロケール強制は LC_CTYPE 環境変数とロケールを C.UTF-8 のような UTF-8 ロケールに設定するので、Python 以外のコードにも影響する。CentOS 7 のようにロケール強制が使えない環境では、POSIX ロケールは UTF-8 モードだけを有効にするため、Python のコードは UTF-8 を使い、Python 以外のコードはロケールの文字コード(POSIX ロケールでは通常 ASCII)を使う、と PEP 540 は書いている。子プロセスから見た振る舞いが Python と外部コマンドで食い違うときは、この違いが原因になりうる。

自分の環境で確かめる手順

公式文書に書かれている関数とオプションだけを使って、手元の Python がどちらの既定で動いているかを見る。出力はここに載せないので、自分の環境で読んでほしい。

python3 -c "import locale, sys; print(sys.getfilesystemencoding(), locale.getpreferredencoding(False), locale.getencoding())"
python3 -X utf8=0 -c "import locale; print(locale.getpreferredencoding(False), locale.getencoding())"
python3 -X utf8 -c "import locale; print(locale.getpreferredencoding(False), locale.getencoding())"
python3 -X warn_default_encoding /path/to/your_script.py

1 行目は現在の既定、2 行目と 3 行目は UTF-8 モードを明示的に切り替えたときの値を並べる。getpreferredencoding(False) と getencoding() の返り値がモードによって食い違うなら、PEP 686 の説明どおり前者が UTF-8 モードの影響を受けている。4 行目は PEP 686 が手引きで挙げている EncodingWarning を出させるためのもので、encoding を省略した open() の場所を探すのに使う。locale.getencoding() は 3.11 で追加された関数なので、それより古い版では 1 行目がエラーになる点に注意してほしい。

文字コードを仕組みから押さえたいとき

文字化けがどう起きるかを仕組みから押さえたいときは、『[改訂新版]プログラマのための文字コード技術入門』(技術評論社)が使えます。技術評論社の書籍ページ(取得日 2026-10-21)の目次には「文字化けのメカニズム」「ラベルと本体の不一致による文字化け」の節があります。例は Java と Ruby で、Python の open() は扱っていません。

コメント

タイトルとURLをコピーしました