PEP 686: Python 3.15でUTF-8モードをデフォルト有効化
(peps.python.org)- Python 3.15はUTF-8モードをデフォルトで有効にし、ファイル、標準入出力、パイプのデフォルトエンコーディングをUTF-8に揃える
- UTF-8はソースファイル、JSON・TOML・YAML、主要エディタ、Webデータ、Node.js・Go・Rust・Javaなどで事実上の標準エンコーディングとして使われており、相互運用性が高まる
- 従来のデフォルトエンコーディングはプラットフォームによって異なり、Unix開発者が
encoding="utf-8"を省略するとWindowsなどで不一致によるバグが発生しうる - 必要であれば
PYTHONUTF8=0または-X utf8=0で無効化でき、互換性確認にはEncodingWarning、encoding="utf-8"、encoding="locale"、locale.getencoding()が使われる - デフォルトエンコーディングに依存するプログラムは、主にWindowsで
UnicodeError、mojibake、静かなデータ破損に遭遇する可能性があり、事前確認が必要
Python 3.15で変わるデフォルトエンコーディング
- PEP 686は、PEP 540のUTF-8モードをデフォルト値として有効化する変更
- デフォルトエンコーディングが必要なファイル、stdio、パイプで、PythonはUTF-8を一貫して使用する
- Python 3.15からデフォルトで有効化され、ユーザーは次の方法で無効化できる
PYTHONUTF8=0-X utf8=0
UTF-8をデフォルトにする理由
- UTF-8は多くの環境で標準的なテキストエンコーディングとして定着している
- PythonソースファイルのデフォルトエンコーディングはUTF-8
- JSON、TOML、YAMLはUTF-8を使用する
- Visual Studio CodeやWindows Notepadを含むほとんどのテキストエディタがUTF-8をデフォルトで使用する
- インターネット上のほとんどのWebサイトとテキストデータがUTF-8を使用する
- Node.js、Go、Rust、Javaを含む多くの人気プログラミング言語がUTF-8をデフォルトで使用する
- PythonのデフォルトエンコーディングがUTF-8に変わると、他のツール・言語・データ形式との相互運用性が向上する
- 多くのUnix環境のPython開発者は、デフォルトエンコーディングがプラットフォーム依存であることを忘れ、JSON・TOML・Markdown・PythonソースファイルのようなUTF-8テキストを読む際に
encoding="utf-8"を省略する - プラットフォームごとのデフォルトエンコーディングの違いは、このようなコードが別の環境で壊れるバグの原因になる
locale APIとencoding="locale"の修正
- UTF-8モードは
locale.getpreferredencoding(False)に影響するため、UTF-8モードに関係なくlocaleエンコーディングを取得するAPIが必要 locale.getencoding()はこの目的のために追加され、localeエンコーディングを返すがUTF-8モードは無視する- このAPIはPython 3.11で追加された
warn_default_encodingオプションが指定されると、locale.getpreferredencoding()はopen()と同様にPEP 597のEncodingWarningを発生させる- PEP 597は
TextIOWrapperにencoding="locale"オプションを追加し、localeエンコーディングを明示的に指定できるようにした - 従来はUTF-8モードで
encoding="locale"を指定しても、TextIOWrapperは"UTF-8"を使用していた- これはPEP 597の動機と合っていなかった
- Pythonのデフォルトテキストエンコーディングが変わる際に、UTF-8モードがデフォルトになる状況を想定していなかったため
- この不一致はPython 3.11で修正され、UTF-8モードでも
encoding="locale"を渡すとlocaleエンコーディングを使用する
後方互換性と移行手順
- ほとんどのUnixシステムはUTF-8 localeを使用し、PythonはlocaleがCまたはPOSIXのときにUTF-8モードを有効化するため、変更の影響は主にWindowsユーザーに集中する
- デフォルトエンコーディングに依存するPythonプログラムは、次の問題に遭遇する可能性がある
UnicodeError- mojibake
- 静かなデータ破損
- 後方互換性の問題を修正する推奨手順は次のとおり
- UTF-8モードを無効化する
- PEP 597の
EncodingWarningで、UTF-8モードが影響する箇所を見つけるencodingオプションが省略されている場合は、encoding="utf-8"またはencoding="locale"の使用を検討するlocale.getpreferredencoding()を使用している場合は、"utf-8"またはlocale.getencoding()の使用を検討する
- UTF-8モードでアプリケーションをテストする
Ruby・Javaの先行事例と却下された代替案
- RubyはRuby 3.0、2020年にWindowsのデフォルト
external_encodingをUTF-8へ変更した - JavaはJDK 18、2022年にデフォルトテキストエンコーディングをUTF-8へ変更した
- RubyとJavaはいずれも後方互換性のためのオプションを提供しているが、Pythonの
EncodingWarningのようなデフォルトエンコーディング使用の警告は提供していない - デフォルトエンコーディングの使用自体を廃止する案は却下された
- ASCIIテキストだけを読み書きする用途でデフォルトエンコーディングを使うケースが多い
- Unixでのみ実行される非クロスプラットフォームアプリケーションには、そのような警告は有用ではない
- すべての場所で
encodingを強制するとユーザー負担が大きく、多数のDeprecationWarningはユーザーが警告を無視する原因になりうる - PEP 387は後方互換性を破る変更に警告の追加を求めているが、必ずしも
DeprecationWarningを求めているわけではない
subprocessモジュールのパイプのデフォルトエンコーディングとしてPYTHONIOENCODINGを使う案も却下された- この方式はUTF-8モードでも
subprocess.Popen(text=True)にレガシーエンコーディングを使えるようにする - しかし「デフォルトエンコーディング」を複雑にし、この方式自体も後方互換性を破る変更である
- ユーザーは
text=Trueをencoding="utf-8"またはencoding="locale"に変えるまで、UTF-8モードを無効化できる
- この方式はUTF-8モードでも
ユーザー教育の観点
- 新規ユーザーは最初の1年間、テキストエンコーディングを学ぶ必要が減る
- 非UTF-8テキストファイルを扱う必要が出たときにエンコーディングを学べばよい
- 既存ユーザーは後方互換性の手順に従って、影響を受ける箇所を確認する
1件のコメント
Hacker News の意見
デフォルトのテキストファイルエンコーディングがプラットフォームによって変わることにはずっとイライラしていたので、今回の変更は歓迎
ファイルシステムのエンコーディングまで触ろうとしていない点も良い。あれは別問題で、それはそれで厄介
Windows が
TextOutAのような ANSI 関数に UTF-8 コードページを使わせる簡単な選択肢を長らく提供しなかったのは大きな失敗だった。manifest ファイルで可能になったのは Windows 10 開発の中盤ごろのことで、こうした機能は NT4 や Windows 98 の時代に入っているべきだったプラットフォームだけでなくユーザーの優先ロケールにも依存しており、C 標準ライブラリも同じように動作する。たとえば Unix/Linux では西欧言語で
iso-8859-1が一般的で、ユーロ導入後は€記号を含むiso-8859-15に切り替えることが多くなった。UTF-8 が問題なく動作し始めたのは 2000 年代後半ごろで、Debian は Etch リリースでデフォルトを UTF-8 に変更した会社のノート PC でのローカルテストはすべてうまくいっていたのに、Linux ホストにデプロイすると、下位アプリケーションが CRLF を要求していて消費できなかった。たまに思い出さなければならない、些細で愚かな問題の一つ。ただし、新しく書かれたソフトウェアがなぜ特定の行終端を要求するのかも、もっともな疑問ではある
不安定なシステムデフォルトに頼らないのは良いこと
こうした値は、ある時点で自分の想定とは違うものを返しがち。数年前に Ubuntu と
init.dスクリプトを扱っていたとき、Java を起動するスクリプトが root で実行されていた。Docker 以前だったのでなおさらだが、一般ユーザーには正常な UTF-8 のデフォルトを設定しないシェルで実行されていた。その結果、OS のデフォルトを使う Java の悪い API 利用が露呈した最近はほとんどの場合、エンコーディングを明示できる API のバリエーションがあり、静的コード検査ツールも間違ったものを使うと警告する。しかし一箇所でも抜けるとコンテンツが壊れ始める。今や UTF-8 ではないエンコーディングの使用は、ほとんどの場合意図しないものの可能性が非常に高く、意図しているなら OS の奇妙な間接設定に頼らず明示すべき。だから良い変更であり、これで壊れるコードには簡単な修正を入れるほうがよい
touch関数が生成した.gitignoreを使っていたが、どうしても Git がそれを尊重しなかった確認してみると、生成されたテキストファイルがUTF-16だったため、実質的に無視されていた。教訓を得てシステムデフォルトを UTF-8 に変えたが、今は単にテキストエディタに頼っている
printf("%f", 4.2)が環境によって魔法のように別の文字列を出力するなら、解決より問題のほうが多くなる。ロケール依存の動作が欲しいときは、関数にロケール情報や関連する部分を明示的に渡すべきこの数十年でますます当てはまるようになったヒューリスティックがある。どこかに
charset設定があって、それがUTF-8 でなければ間違いというものPython 2 は文字集合に無関心だったので常に動いたが、Python 3 の改善は単純な改善だけではなかった。Python 3 スクリプトと Python 2 スクリプトを見分ける方法はこうだ。文字列
utf-8が入っていれば Python 3 で、C.UTF-8ロケールでしか動かないなら Python 3。今回の変更は Python 3 を「修理」するもののように理解できるので歓迎するPython 3 からデフォルトだと思っていた
u""接頭辞が不要になった文字列のことを思い浮かべているのだろうたった今 Python 2.7 で
"éķů"を入力してみたところ、その文字の UTF-8 バイト列が出力されたので、u接頭辞が正確に何をしていたのかはよく分からない。ただ、Python 2 から 3 への移行における大きな変化の一つは、文字列がエンコーディングを持ち、バイト文字列はエンコーディングのないバイト列になったこと。今回の変更は主に、Windows のようにデフォルトエンコーディングが UTF-8 ではない環境でopen('filename', mode='r')を使う際に、open('filename', mode='r', encoding='UTF-8')と明示する必要があった問題に関するものに見えるPath("filenames use their own encoding").write_text("file content encoding uses yet another encoding")のように、文字列リテラル、ファイル名、ファイル内容のエンコーディングはそれぞれ異なる。対応するエンコーディングはtokenize.openの UTF-8、os.fsencodeのsys.getfilesystemencoding()、openのlocale.getpreferredencoding()「Node.js、Go、Rust、Java を含む他の人気プログラミング言語も UTF-8 をデフォルトで使う」とは、Java がUTF-16 から UTF-8 に移ったことを見落としていた
Stringクラス内部では UTF-16 と latin-1 エンコーディングが今も使われており、JVM は以前と同じく modified UTF-8 エンコーディングを使う。Stringクラスはもともと UTF-16 だけを使っていたが、Java 9 以降は可能な場合、1 文字あたり 1 バイトの latin-1 エンコーディングも使うJava は読み書きのエンコーディングのデフォルトとして UTF-16 を使ったことはない
CPython の内部エンコーディングはいま UTF-8 なのか?
Python の文字列は添字でインデックス指定できるが、ランダムアクセスは十分まれなので、必要なときに遅延インデックス化してもよさそう。1 つ前や後ろへ進むだけならインデックスは不要なので、内部表現を UTF-8 にすることも十分可能ではある
strを表現しているのはPyUnicodeオブジェクトUTF-8 バイトが要求されると、必要に応じて
bytesオブジェクトが生成され、PyUnicodeの一部としてキャッシュされ、PyUnicodeが解放されると一緒に解放される。別途、文字列を構成するコードポイントはランダムアクセスできるよう単純な配列に格納される。各コードポイントのサイズは 1、2、4 バイトになり得て、PyUnicodeを作るときに最大コードポイント値を指定すると、127、255、65535、1,114,111 のいずれかに切り上げられ、1/2/4 バイトのどれを使うかが決まる最大コードポイント値が 127 なら、その配列表現を UTF-8 としてそのまま使える。なので質問への答えは、すべてのコードポイントが 127 以下である多くの文字列は UTF-8 として保存される、ということ。ただし文字列を走査するときにコードポイント単位で考えるべきではない。ユーザーが認識する文字、つまり書記素クラスタは 1 つ以上のコードポイントから成る。たとえばアクセント付きの e は
eのコードポイントの後に結合アクセントのコードポイントが続くことがあり、フェニックスの絵文字は鳥の絵文字、ゼロ幅接合子、火の絵文字で構成される。何億人もの人が使う一部の文字体系も、子音に母音を表す結合記号が付く方式に近い。この- -はコードポイント 5 個であり、複数の言語がその「長さ」をどう報告するかを扱った良い記事がある: https://hsivonen.fi/string-length/。これは、この部分を扱う Unicode TR29 を Python C 拡張として実装してみた経験からの話なぜ
utf-8-sigではないのか気になる。任意の BOM を処理してくれるし、先週もそれのせいでスクリプトを直す必要があった推奨もされていないし、最近では BOM で失敗する挙動も妥当だと思う
UTF-8 の話なら、Linux フレームバッファはとっくにまともな UTF-8 サポートを備えているべきだった
256/512 グリフのものではなく、本物のサポートのこと。GNU Hurd ですら 2007 年ごろから UTF-8 をサポートするより良い端末コンソールがあったのに、いまは 2024 年だ
良い。あとは JS が UTF-8 に変えるだけ
もちろん JS は改善できない。ほかのどのプログラミング言語とも違って、1995 年に書かれたコードとの互換性が必要だから
文字列の内部表現は別問題で、JavaScript と同じく Python も内部で「単に UTF-8」を使っているわけではない
「Unix を使う多くの Python 開発者は、デフォルトエンコーディングがプラットフォーム依存であることを忘れ、UTF-8 でエンコードされたテキストファイルを読むときに
encoding="utf-8"を省略する」について、これは忘れたというより十分に知られていないのかもしれない正直、Python は明示的に別の指定をしない限り、どこでも UTF-8 だけを使うと思っていた
bytes.decodeとstr.encodeは、少なくとも Python 3 以降は UTF-8 をデフォルトにしてきた。一方、ファイル名をデコードするときのデフォルトエンコーディングはsys.getfilesystemencoding()を使い、Windows と macOS ではこれも UTF-8 だが、Linux ではロケール、具体的にはCODESETによって変わる。最後に、openはlocale.getencoding()を直接使う