本記事はアフィリエイトリンクを含みます。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 は「後方互換の問題を直す手引き」として次の順番を示している。
- UTF-8 モードを無効にする。
EncodingWarning(PEP 597)を使って、UTF-8 モードが影響する箇所をすべて見つける。- encoding を省略している箇所は
encoding="utf-8"かencoding="locale"に書き換える。locale.getpreferredencoding()を使っている箇所は"utf-8"かlocale.getencoding()に置き換えることを検討する。 - 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() は扱っていません。

コメント