はじめに
2026年10月9日、Python 3.15.0 が公開されました。3.14 からの差分は 5,643 コミット・1,012 人の貢献で、言語・実装・標準ライブラリにまたがる大きな更新です。目玉は明示的な lazy import(PEP 810)、組み込み型 frozendict(PEP 814)と sentinel(PEP 661)、そして UTF-8 の既定エンコーディング化(PEP 686) です。
このうち、日本語の Windows でスクリプトを書いてきた人に直撃するのが PEP 686 です。これまで open("memo.txt") のように encoding を省くと、日本語版 Windows では cp932(Shift_JIS 系)で読み書きされていました。3.15 からは OS の設定に関係なく UTF-8 になります。Excel が吐いた CSV を読むスクリプトや、cmd の出力を subprocess で拾うスクリプトは、動きが変わる可能性があります。逆に、UTF-8 の JSON を読むのに encoding を書き忘れて落ちていたスクリプトは、何もしなくても通るようになります。
この記事のゴールは、手元の自作スクリプトが 3.15 で何が変わり、どこを直せばよいかを自分で判定できる状態になることです。そのために「既定エンコーディング」とは何かという基礎から始め、3.14 と 3.15 の挙動の対比表、lazy import の仕組みと落とし穴、JIT と tail-calling インタプリタの数字を一次資料で整理し、最後に 3.14 と 3.15 を同じ PC に並べて確かめる手順を置きます。
- 3.15 では encoding を省いたテキスト I/O が、OS の設定に関係なく UTF-8 になる(PEP 686)。戻すなら
PYTHONUTF8=0か-X utf8=0 - 日本語 Windows で影響を受けるのは、cp932 のファイルを encoding 無しで読む処理と、
subprocessのtext=Trueでcmdなどの出力を受ける処理。後者は手元の Windows 11 で、例外が上がらずr.stdoutがNoneになる形で現れた。直し方はencoding="utf-8"かencoding="locale"の明示 lazy importは起動時に読み込まず、最初に使った瞬間に読み込む。import 時の副作用に頼るモジュール(登録パターン)は対象にしない
📝 1. テキストの「既定エンコーディング」とは
文字列とバイト列の間に立つもの
Python の文字列 str は Unicode の文字の並びで、ファイルやパイプを流れるのはバイト列です。この間を変換する規則が エンコーディング で、たとえば「あ」は UTF-8 なら 3 バイト、cp932 なら 2 バイトになります。
open("memo.txt", encoding="utf-8") のように明示すれば迷いはありません。問題は省いたときです。Python は「既定エンコーディング」を使いますが、3.14 まではこれが OS と地域設定で変わる値 でした。
Windows はなぜ cp932 だったか
Windows には「ANSI コードページ」という、古い API 向けのシステム既定の文字コードがあります。日本語版 Windows ではこれが 932 で、Microsoft が Shift_JIS を拡張した cp932 です。Python はこの値を locale.getencoding() で取得し、UTF-8 モードが無効なときはテキストファイルの既定エンコーディングとして使います(公式ドキュメント「Using Python on Windows」)。Linux や macOS では地域設定が UTF-8 なのが普通なので、同じスクリプトが Windows でだけ落ちる、という現象が生まれていました。
PEP 686 の動機の節には、Unix で開発する多くの Python 開発者が「既定エンコーディングはプラットフォーム依存」であることを忘れ、UTF-8 の JSON や Markdown を読むときに encoding="utf-8" を書き落とす、とあります。PEP 597 は、PyPI のダウンロード上位 4,000 パッケージのうち 489 が README に非 ASCII 文字を含み、82 が encoding の指定漏れのため非 UTF-8 ロケールでソースからインストールできなかった、と数えています。
cp932 は Microsoft が Shift_JIS を拡張した文字コードで、Windows の日本語環境の ANSI コードページです。①や㈱などの機種依存文字が入っています。UTF-8 と違い、µ(マイクロ記号 U+00B5)や一部の記号を表せません。Python のコーデック名では cp932 のほか、Windows の ANSI コードページそのものを指す別名 mbcs も使えます。
UTF-8 モードと、そこへ至る道のり
Python は一気に切り替えたわけではなく、段階を踏んできました。
| 版 | PEP | 変わったこと |
|---|---|---|
| 3.6 | PEP 528 / PEP 529 | Windows の コンソール I/O と ファイルシステム(パス名) を UTF-8 に。リダイレクトされた標準出力やテキストファイルは対象外 |
| 3.7 | PEP 540 | UTF-8 モード を追加。-X utf8 か PYTHONUTF8=1 で有効化(既定は無効) |
| 3.10 | PEP 597 | encoding を省いた箇所を見つける EncodingWarning(-X warn_default_encoding)と、地域設定を明示する encoding="locale" を追加 |
| 3.11 | PEP 686 の準備 | UTF-8 モードでも地域設定の値を返す locale.getencoding() を追加。encoding="locale" が UTF-8 モードでも地域設定を使うよう修正 |
| 3.15 | PEP 686 | UTF-8 モードを既定で有効に。PYTHONUTF8=0 か -X utf8=0 で以前の挙動に戻せる |
PEP 686 の著者は Inada Naoki 氏です。2022年3月に起草され、Python 3.15 を対象として受理されていました。Ruby は 3.0(2020年)で Windows の既定を UTF-8 に、Java は JDK 18(2022年)で既定のテキストエンコーディングを UTF-8 に変えており、PEP はこれらを先行例として挙げています。
既定エンコーディングは、どこで決まるか
encoding を指定したか"] -->|"指定あり"| B["その指定どおり
utf-8 / cp932 / locale"] A -->|"指定なし"| C{"UTF-8 モードは有効か"} C -->|"3.14 以前の既定:無効"| D["locale.getencoding()
日本語 Windows では cp932"] C -->|"3.15 の既定:有効"| E["UTF-8"] F["PYTHONUTF8=0 / -X utf8=0
で 3.15 でも無効にできる"] -.-> C
左の分岐、つまり encoding を明示しているコードは 3.14 でも 3.15 でも同じ結果 になります。変わるのは右の分岐、省いたときの既定だけです。
3.14 と 3.15 の挙動対比(日本語 Windows)
公式ドキュメントの記述(UTF-8 モードの仕様・sys.stdout の仕様・Windows の節)から、日本語版 Windows での既定の挙動を場面ごとに並べました。
| 場面 | 3.14 以前(UTF-8 モード無効が既定) | 3.15(UTF-8 モード有効が既定) |
|---|---|---|
open("a.txt")(encoding 省略) |
cp932 | UTF-8 |
open("a.txt", encoding="locale") |
cp932 | cp932(地域設定を明示) |
open("a.txt", encoding="mbcs") |
cp932 | cp932(ANSI コードページを明示) |
subprocess.run(..., text=True) の stdout / stderr |
cp932 | UTF-8 |
sys.stdout がコンソールに直結のとき |
UTF-8(PEP 528・変わらず) | UTF-8 |
sys.stdout がパイプやファイルにリダイレクトされたとき |
cp932 | UTF-8(エラーハンドラは surrogateescape) |
locale.getpreferredencoding(False) |
cp932 |
utf-8 |
locale.getencoding() |
cp932 |
cp932(UTF-8 モードの影響を受けない) |
| ファイル名・コマンドライン引数 | UTF-8(PEP 529・変わらず) | UTF-8 |
open() の errors の既定 |
strict |
strict(変わらず。壊れたデータを黙って通さない) |
何が直り、何が壊れうるか
直る側
- UTF-8 で保存した JSON・TOML・Markdown を encoding 無しで読んでいて、日本語や絵文字で
UnicodeDecodeErrorになっていたスクリプトは、そのまま通る print()の出力をファイルやパイプに流したとき、µ・Ω・絵文字でUnicodeEncodeError: 'cp932' codec can't encode characterが出ていたものは、出なくなる- Linux で書いたスクリプトを Windows に持ってきたときの差が小さくなる
壊れうる側
- cp932 で保存されたファイル(日本語版 Excel の「CSV(コンマ区切り)」形式など)を encoding 無しで読むと、
UnicodeDecodeErrorで止まる。errorsの既定がstrictなので、黙って文字化けするのではなく例外になる subprocess.run(["cmd", "/c", "dir"], capture_output=True, text=True)のように、cp932 で出力する Windows のコマンドの結果を text モードで受けると、復号に失敗する。手元の Windows 11 で試すと、UnicodeDecodeErrorは呼び出し側に上がらず、出力を読む別スレッドの例外として標準エラーに出て、r.stdoutがNoneになった(第 5 節)。気づくのは.strip()した時点のAttributeErrorなので、原因が分かりにくい- cp932 を期待する古いツールへ渡すファイルを encoding 無しで書くと、Python 側はエラーにならず、相手側で文字化け する。PEP 686 も、既定エンコーディングに依存したプログラムでは「UnicodeError・文字化け・場合によっては気づかないデータ破損」が起こりうると書き、大きく告知すべきだとしています
直し方 — PEP 686 のガイドライン
PEP 686 の「Backward Compatibility」の節に、順番つきの手引きがあります。
- 止血:環境変数
PYTHONUTF8=0か起動オプション-X utf8=0で、UTF-8 モードを無効にする - 洗い出し:
python -X warn_default_encoding script.py(またはPYTHONWARNDEFAULTENCODING=1)で実行し、encoding を省いたopen()ごとに出るEncodingWarningを集める - 直す:省いた箇所に
encoding="utf-8"を、地域設定で保存されたファイルを読む箇所にはencoding="locale"を書く locale.getpreferredencoding()を使っていたら、"utf-8"かlocale.getencoding()に置き換える- UTF-8 モード(=3.15 の既定)でテストする
# 3.14 でも 3.15 でも同じ結果になる書き方
import subprocess
# 自分で作る、UTF-8 と分かっているファイル
with open("memo.txt", encoding="utf-8") as f:
text = f.read()
# OS の地域設定(日本語 Windows なら cp932)で保存されたファイル
with open("export.csv", encoding="locale") as f:
rows = f.read().splitlines()
# cmd の出力は cp932。text=True の代わりに encoding を明示する
r = subprocess.run(["cmd", "/c", "dir"], capture_output=True, encoding="locale")
print(r.stdout)
subprocess の text=True は「io.TextIOWrapper の既定で開く」という意味なので、3.15 では UTF-8 になります。encoding= を渡せばその値が優先され、"locale" も使えます。
もう 1 つ、移行前のテストに使える事実があります。UTF-8 モードは 3.7 から存在するので、3.14 のまま -X utf8=1 を付けて走らせれば、3.15 の既定と同じ挙動を先に試せます。手元の Windows 11 でも、3.14.8 に -X utf8=1 を付けたときの結果は 3.15.0 の既定と一致しました(第 5 節)。
公式ドキュメントの Windows の節は、PYTHONUTF8 を既定の環境変数に入れると 3.7 以降のすべての Python アプリケーションに影響する と注意しています。古い挙動が要る場合は、そのプロセスだけ一時的に設定するか、-X utf8=0 を起動オプションで渡す形が勧められています。
⚡ 2. lazy import とは
起動時間のどこに import が効くか
モジュールを import すると、Python は ①ファイルを探し ②読み込み ③バイトコードにコンパイルし ④トップレベルのコードを実行します。依存が深いアプリケーションでは、これが秒単位になります。しかも、その実行で実際には使わないモジュールにも同じコストを払っていました。
これまでの回避策は、import を関数の中に移す、importlib で必要になった時点で読み込む、依存を減らすよう構成し直す、のどれかでした。どれも動くものの、import 文がコードベースに散らばって読みにくくなり、守り続けるのに規律が要る、と PEP 810 は書いています。
PEP 810 の構文
3.15 では、ソフトキーワード lazy を import 文の前に置くと、実際の読み込みがその名前を最初に使うまで先送り されます。先送りの間、名前には軽いプロキシオブジェクトが入っています。
lazy import json
lazy from pathlib import Path
print("Starting up...") # ここでは json も pathlib もまだ読み込まれていない
data = json.loads('{"key": "value"}') # ここで json が読み込まれる
p = Path(".") # ここで pathlib が読み込まれる
import の宣言をファイルの先頭にまとめる読みやすさを保ちながら、読み込みのコストは使ったモジュールの分だけ払う、という設計です。
| 項目 | 内容 |
|---|---|
| 書ける場所 | モジュールのトップレベルだけ。関数・クラス本体・try/except/finally の中では SyntaxError |
| 使えない形 | lazy from m import * と lazy from __future__ import ... は SyntaxError。星付き import は常に即時 |
| 失敗したとき | モジュールが無いなどの例外は、import 文ではなく 最初に使った場所 で起きる。トレースバックには使った場所と元の import 文の両方が出る |
| 一括で有効化 | -X lazy_imports=all か環境変数 PYTHON_LAZY_IMPORTS=all。既定の normal はソースの lazy だけを尊重する。実行時は sys.set_lazy_imports() / sys.get_lazy_imports() |
| 選んで有効化 | sys.set_lazy_imports_filter() に関数を渡す。引数は(import する側のモジュール名・される側の名前・fromlist)で、True を返したものだけ lazy になる |
| 古い版との両立 | モジュールに __lazy_modules__ = ["json", "pathlib"] を書くと、通常の import 文がそのモジュールについて lazy 扱いになる。3.15 より前では無視され、普通の import として動く |
| プロキシの型 | types.LazyImportType |
落とし穴 — 「読み込まれるのは、使った瞬間」
PEP 810 は後方互換性の節で、lazy を付けた import に限って観測できる変化を 5 つ挙げています。
- エラーの時機:
ImportErrorや存在しない名前のAttributeErrorが、import 文ではなく使った時点で出る - 副作用の時機:モジュールが import 時に行う処理(登録・設定)は、最初に使うまで実行されない
- import の順序:使った順に読み込まれるので、ソース上の順序と一致しない
sys.modulesへの登場:最初に使うまでsys.modulesに現れない- プロキシの見え方:使う前に名前の中身を覗く処理は、実体ではなくプロキシを見る
いちばん踏みやすいのは 2 つ目です。デコレータでプラグインを登録するようなコードは、import された瞬間に登録が走ることを前提にしています。
# my_plugin.py — import された瞬間に自分を登録する
from plugin_registry import register_plugin
@register_plugin("MyPlugin")
class MyPlugin:
pass
# main.py
lazy import my_plugin # まだ読み込まれていない → 登録もされていない
PEP 810 は、こうした登録は import の副作用に頼らず、discover_plugins() のような関数を明示的に呼ぶ形に変えることを勧めています。あわせて、import foo のあとで foo.bar.Baz と書く形も避けるべきだとしています。foo.bar という属性は、どこかで foo.bar が import された副作用で付いているだけなので、import foo.bar と明示します。
ライブラリ側の相性については、PEP 810 の FAQ が「import 時の副作用(登録・モンキーパッチ)に依存するもの」「import の順序を前提にするもの」「import 時にグローバルな状態を変えるもの」を要注意として挙げています。一括で有効にして問題が出たら、フィルタで除外するか、-X lazy_imports=none で全部切って切り分けます。
効果はどれくらいか
3.15 の What’s New は、lazy import による起動時間の短縮を数値では示していません。PEP 810 が引いているのは Meta(Cinder)や Hudson River Trading の大規模な事例と、Qt for Python(PySide)で 10〜20% の起動時間短縮が出た実装例で、効果はコードベースの規模と相互依存の深さで決まる、と書かれています。つまり自分のスクリプトでどれだけ効くかは、測るしかありません。第 5 節で手順を置きます。
🚀 3. JIT と tail-calling インタプリタとは
tail-calling インタプリタ:命令の受け渡し方を変えた
CPython は Python のコードをバイトコードに変換し、1 命令ずつ実行するインタプリタです。従来は 1 つの巨大な switch 文(C 言語)で命令を分岐していました。3.14 で、命令ごとの処理を小さな C 関数に分け、末尾呼び出しで次の命令の関数へ渡す 方式が追加されました。3.14 の What’s New によると、pyperformance ベンチマークの幾何平均で 3〜5% 高速(Clang 19 でビルドした 3.14 が基準)で、当時は Clang 19 以降でしかビルドできない opt-in の機能でした。Python プログラムの見た目の挙動は変わりません。Python 関数の末尾呼び出し最適化とは別物で、そちらは CPython にはありません。
3.15 では、python.org が配る Windows 64bit 版のバイナリがこの方式になりました。Visual Studio 2026(MSVC 18)に入った新機能で可能になったもので、What’s New の数字は次のとおりです。
| 測定 | 条件 | 結果 |
|---|---|---|
| pyperformance の幾何平均 | Windows x86-64・AMD Ryzen 7 5800X・Visual Studio 18.1.1・switch-case インタプリタ比 | 15〜20% 高速 |
| 観測された範囲 | 大きな純 Python ライブラリ 〜 長く走る小さな純 Python スクリプト | 14% 〜 40% 高速 |
Windows で 3.14 から 3.15 に上げるだけで受け取れる速度差は、JIT ではなくこちらです。
JIT:実験段階の機械語生成
JIT(Just-In-Time コンパイラ)は、実行中に頻繫に通る経路を機械語に落とす仕組みです。CPython の JIT は実験的な機能で、3.15 では大きく手が入りました。
| 測定 | 条件 | 結果 |
|---|---|---|
| pyperformance の幾何平均 | x86-64 Linux・全最適化つきの標準インタプリタ比 | 7〜8% 高速 |
| pyperformance の幾何平均 | AArch64 macOS・全最適化つきの tail-calling インタプリタ比 | 11〜12% 高速 |
| 個別ベンチマークの範囲 | JIT あり vs なし(unpack_sequence を除く) | 約 15% 低下 〜 100% 超の向上 |
中身の変化は、ビルド時の LLVM 21 への更新(実行には不要)、実際の実行経路を記録する新しいトレーシングフロントエンド(Windows 対応も追加)、基本的なレジスタ割り当て、定数伝播の拡充、安全な場合の参照カウント更新の省略、GDB からの JIT フレームの巻き戻し対応などです。
3.14 の What’s New によると、公式の macOS と Windows のバイナリには実験的 JIT が同梱され、環境変数 PYTHON_JIT=1 で有効にでき、sys._jit.is_available() と sys._jit.is_enabled() で状態を確かめられます。3.15 の What’s New には同梱の有無の記述が見当たらなかったので、手元のバイナリで sys._jit.is_available() を見るのが確実です。
観測しやすくする 2 つの変更
- PEP 831 フレームポインタの既定有効化:対応するプラットフォームでは
-fno-omit-frame-pointerなどの付いたビルドになり、perf や eBPF 系のツール、デバッガ、クラッシュ解析がネイティブのスタックを速く確実にたどれるようになります。フラグはsysconfig経由で拡張モジュールにも伝わります。What’s New は、独自のビルドシステムで C/C++/Rust のネイティブコードを作るときも同等のフラグを付けること、1 つでも欠けるとプロセス全体の巻き戻しが途切れることを注意しています - PEP 799
profilingパッケージと Tachyon:既存のプロファイラをprofiling.tracingに整理し、新しいサンプリングプロファイラ Tachyon をprofiling.samplingとして追加。動いているプロセスに PID で取り付けてコードを変えずに計測でき、最大 1,000,000 Hz のサンプリング、wall / cpu / gil / exception の 4 モード、pstats・flame graph(HTML)・Firefox Profiler 形式・行単位のヒートマップの出力、--liveの TUI を備えます
📋 4. 3.15 の主な変更(一覧)
リリースページと What’s New の見出しにもとづく一覧です。リンクは一次資料です。
| 分類 | 変更 | 一行で | 一次 |
|---|---|---|---|
| 言語 | PEP 810 lazy import | lazy import で読み込みを最初の使用時まで先送り |
What’s New |
| 言語 | PEP 686 UTF-8 既定化 | encoding 省略時の既定が UTF-8 に。PYTHONUTF8=0 で戻せる |
What’s New |
| 言語 | PEP 814 frozendict |
変更不可でハッシュ可能な辞書。dict の派生ではない |
What’s New |
| 言語 | PEP 661 sentinel |
一意な番兵値を簡潔な表現で作る組み込み型 | What’s New |
| 言語 | PEP 798 内包表記のアンパック | [*L for L in lists] で入れ子の内包や itertools.chain() を置き換え |
What’s New |
| 言語 | PEP 829 パッケージ起動設定ファイル | パッケージ単位の起動時設定 | PEP 829 |
| 実装 | 実験的 JIT の更新 | x86-64 Linux 7〜8%・AArch64 macOS 11〜12%(pyperformance 幾何平均) | What’s New |
| 実装 | エラーメッセージの改善 | [1,2].push(4) に「.append のことか」、container.area に「.inner.area のことか」と提案 |
What’s New |
| 標準ライブラリ | PEP 799 profiling パッケージ・Tachyon |
プロファイラの整理と高頻度サンプリングプロファイラ | What’s New |
| 標準ライブラリ | 色付き出力の拡大 | python --help・argparse・difflib・unittest などの CLI が色付きに |
What’s New |
| 標準ライブラリ | math.integer |
整数向け数学関数の新モジュール | What’s New |
| 標準ライブラリ | subprocess.Popen.wait() |
timeout 指定時に Linux は pidfd、macOS/BSD は kqueue で待つ。Windows は従来どおり | What’s New |
| 型ヒント | PEP 728 / 747 / 800 | TypedDict の追加項目の型・TypeForm・型システムの disjoint base |
What’s New |
| C API | PEP 782 / 788 / 803・820・793 | PyBytesWriter・終了処理からの保護・free-threaded ビルド向け Stable ABI |
What’s New |
| ビルド | PEP 831 フレームポインタ | 既定で有効。システムレベルの観測性の向上 | What’s New |
| 配布 | Windows 64bit バイナリ | tail-calling インタプリタに。pyperformance 幾何平均で 15〜20% 高速(Ryzen 7 5800X) | What’s New |
| 配布 | macOS バイナリ | free-threading 対応を既定でインストール | リリースページ |
移行で引っかかりやすい変更は、What’s New の「Porting to Python 3.15」にまとまっています。sqlite3.connect() の database 以外の引数がキーワード専用になった、argparse で短いオプションと 1 ダッシュの長いオプションを併記したときの dest の推論が変わった、base64.urlsafe_b64decode() がパディング無しを受け付けるようになった、unittest の assertWarns() が条件に合わない警告を飲み込まなくなった、などです。
公式ドキュメントの Windows の節では、従来の フルインストーラ(.exe)は 3.14 から非推奨 で、3.16 以降は作られないと書かれています。後継は Python install manager で、Microsoft Store か python.org から入れると python・py・pymanager コマンドが使えるようになります。3.15.0 の時点では .exe のインストーラもダウンロードページに並んでいますが、複数の版を並べて使うなら install manager の py install 3.14 / py install 3.15 が勧められています。従来の py.exe ランチャーも非推奨の扱いです。
🧪 5. 手元で確かめる手順と結果(Windows 11・3.14 と 3.15 を並べる)
ここからは、上の表が自分の PC でそのとおりになるかを確かめる手順です。結果の欄には、この記事のために Windows 11 Pro(x64・日本語・システムロケールとコンソールのコードページは 932)で、python.org の embeddable 版 3.14.8 と 3.15.0 を並べて動かした値 を入れました。時間はいずれも複数回の最小値で、絶対値は PC によって変わります。見るべきは 3.14 と 3.15 の差です。embeddable 版は ._pth の仕組みで site を読まない(sys.flags.isolated が 1)配布形態ですが、ここで見る挙動はインストーラ版と同じはずです。
本節のコマンドは コマンドプロンプト(cmd.exe)で実行する前提 です。PowerShell はネイティブコマンドの出力を自分のコンソール設定で復号し、リダイレクトのときに再符号化するため、エンコーディングの確認にはかえって混乱のもとになります。
python -I(隔離モード)を付けると、PYTHONUTF8などの環境変数が無視されます。環境変数の効き方を見るときは-Iを外します- 結果を
print()で出すとき、標準出力がパイプだと 3.14 では cp932 になるので、U+FFFD(�)や en dash(–)のような cp932 に無い文字でUnicodeEncodeErrorが出ます。結果はencoding="utf-8"を明示したファイルに書いて開くのが確実です
5-1. 3.14 と 3.15 を同じ PC に入れる
Python install manager(Microsoft Store か python.org の Downloads)を入れたうえで、2 つの版を追加します。
py install 3.14
py install 3.15
py list
py -V:3.14 -c "import sys; print(sys.version)"
py -V:3.15 -c "import sys; print(sys.version)"
-V:3.14 のように版を指定して起動できます。従来のランチャーの py -3.14 という形も、互換のために残っています。この記事の結果は、インストールせずに zip を展開するだけで使える embeddable 版(python-3.14.8-embed-amd64.zip / python-3.15.0-embed-amd64.zip)を別フォルダに置き、それぞれの python.exe を直接呼んで取りました。
5-2. 既定エンコーディングの違いを見る
次のスクリプトを enc_probe.py として保存します。出力は ASCII だけにして、確認の対象そのものが文字化けしないようにしています。最後の 2 行は、encoding を省いて書いた「日本語」が何バイトになるかを見るものです。
import io
import locale
import sys
from pathlib import Path
print("python :", sys.version.split()[0])
print("utf8_mode :", sys.flags.utf8_mode)
print("open_default:", io.text_encoding(None)) # "utf-8" か "locale"
print("preferred :", locale.getpreferredencoding(False))
print("getencoding :", locale.getencoding()) # 地域設定そのもの
print("fs_encoding :", sys.getfilesystemencoding())
print("stdout :", sys.stdout.encoding, sys.stdout.errors)
Path("written.txt").write_text("日本語") # encoding を省いて書く
print("written :", Path("written.txt").read_bytes()[:9].hex(" "))
ファイルにリダイレクトして(=標準出力をパイプ扱いにして)実行します。
py -V:3.14 enc_probe.py > out314.txt
py -V:3.14 -X utf8=1 enc_probe.py > out314u.txt
py -V:3.15 enc_probe.py > out315.txt
py -V:3.15 -X utf8=0 enc_probe.py > out315n.txt
type out314.txt out314u.txt out315.txt out315n.txt
| 実行 | utf8_mode | preferred | getencoding | stdout(パイプへ出力時) | encoding を省いて書いた「日本語」 |
|---|---|---|---|---|---|
| 3.14.8 既定 | 0 | cp932 | cp932 | cp932 | 93 fa 96 7b 8c ea(cp932・6 バイト) |
3.14.8 -X utf8=1 |
1 | utf-8 | cp932 | utf-8 | UTF-8 |
| 3.15.0 既定 | 1 | utf-8 | cp932 | utf-8 | e6 97 a5 e6 9c ac e8 aa 9e(UTF-8・9 バイト) |
3.15.0 -X utf8=0 |
0 | cp932 | cp932 | cp932 | cp932 |
第 1 節の対比表どおりの結果です。getencoding が 3.15 でも cp932 のままなのは、OS のロケールは何も変わっていないからで、cp932 のデータを読むときは encoding="locale" か encoding=locale.getencoding() と書けば、どの版でも地域設定の値を使えます。fs_encoding は両方 utf-8 でした。3.14.8 に -X utf8=1 を付けた行が 3.15.0 の既定と同じ値になっている点が、移行前のテストに使えます。コンソールに直接出したときの stdout は、PEP 528 により両方 UTF-8 になるはずですが、ここでは測っていません。
5-3. cp932 前提の open() と subprocess がどう振る舞うか
cp932_probe.py として保存します。cp932 のファイルを自分で作って encoding を省いて読み返す、cp932 で出力するコマンドを text=True で受ける、という最小の再現です。(4) で r.stdout を repr() で出しているのは、後述のとおり None になる場合があるからです。
import locale
import subprocess
from pathlib import Path
# (1) cp932 で保存された CSV を、encoding を省いて読む(3.14 以前によくある書き方)
src = Path("export_cp932.csv")
src.write_bytes("品名,数量\n抵抗 1kΩ,100\n".encode("cp932"))
try:
with open(src) as f:
print("(1) read :", f.read().splitlines()[1])
except UnicodeDecodeError as e:
print("(1) read NG :", e)
# (2) 同じファイルを encoding="locale" で読む
with open(src, encoding="locale") as f:
print("(2) locale :", f.read().splitlines()[1])
# (3) UTF-8 のメモを、encoding を省いて読む
utf = Path("notes_utf8.md")
utf.write_text("# メモ\nµC の消費電流\n", encoding="utf-8")
try:
with open(utf) as f:
print("(3) utf8 read :", f.read().splitlines()[1])
except UnicodeDecodeError as e:
print("(3) utf8 NG :", e)
# (4) cp932 で出力する Windows コマンドを text=True で受ける
try:
r = subprocess.run(["cmd", "/c", "echo", "日本語"], capture_output=True, text=True)
print("(4) text=True :", repr(r.stdout))
except Exception as e:
print("(4) text NG :", type(e).__name__, e)
# (5) 同じコマンドを、地域設定のエンコーディングを明示して受ける
r = subprocess.run(["cmd", "/c", "echo", "日本語"], capture_output=True,
encoding=locale.getencoding())
print("(5) explicit :", repr(r.stdout))
py -V:3.14 cp932_probe.py > probe314.txt 2>&1
py -V:3.15 cp932_probe.py > probe315.txt 2>&1
py -V:3.15 -X utf8=0 cp932_probe.py > probe315n.txt 2>&1
py -V:3.15 -X warn_default_encoding cp932_probe.py > probe315w.txt 2>&1
| 番号 | 場面 | ドキュメントから予想される挙動 | 3.14.8 既定 | 3.15.0 既定 |
|---|---|---|---|---|
| (1) | cp932 ファイルを encoding 無しで読む | 3.14 は読める。3.15 は UnicodeDecodeError |
読めた | UnicodeDecodeError(0x94 のバイトで停止) |
| (2) | 同じファイルを encoding="locale" で読む |
どちらも読める | 読めた | 読めた(-X utf8 の有無に関係なく) |
| (3) | UTF-8 ファイルを encoding 無しで読む | 3.14 は文字化けか例外。3.15 は読める | UnicodeDecodeError(cp932 codec・0x8a のバイトで停止) |
読めた |
| (4) | echo 日本語 の出力を text=True で受ける |
3.14 は読める。3.15 は復号に失敗 | '日本語\n' が読めた |
r.stdout が None。標準エラーに Exception in thread Thread-3 (_readerthread) と UnicodeDecodeError が出る |
| (5) | 同じ出力を encoding=locale.getencoding()(="cp932")で受ける |
どちらも読める | 読めた | 読めた |
| ― | 3.15.0 に -X utf8=0 を付ける |
(1)(3)(4) が 3.14 と同じになる | ― | (1)(3)(4) とも 3.14.8 と同じ結果 |
(4) が肝です。subprocess は子プロセスの出力を別スレッドで読んでいて、そこで起きた UnicodeDecodeError は呼び出し側には上がりません。結果として r.stdout が None になり、次に .strip() や .splitlines() を呼んだところで AttributeError: 'NoneType' object has no attribute 'strip' になります。エラーの文面にエンコーディングの気配が無いので、3.15 に上げて初めてこれを見た人は、原因に届くまで遠回りすると思われます。対策は (5) のように encoding を明示することで、errors="replace" を添えて落とさない選択や、コンソールのコードページを chcp 65001 で UTF-8 に変えておく手もあります。
(3) は「直る側」の実例です。UTF-8 で書いた「# メモ/µC の消費電流」を encoding 無しで読むと、3.14.8 の既定では cp932 として復号しようとして 0x8a のバイトで UnicodeDecodeError になり、3.15.0 の既定ではそのまま読めました。モードを入れ替えると結果も入れ替わり、3.14.8 に -X utf8=1 を付ければ読め、3.15.0 に -X utf8=0 を付ければ同じ例外になります。予想では「文字化けか例外」と書いていましたが、このバイト列では例外でした。UTF-8 のバイト列を cp932 として読むと、並びによっては例外にならず別の文字に化けて通ることもあるので、読めてしまった場合も中身は疑ってください。(2) の encoding="locale" は、版とモードのどちらにもよらず OS の地域設定(cp932)で読めたので、古いデータの読み込みにはこれを明示すれば足ります。
5-4. 起動時間と lazy import の効き目を測る
まず、使わない import がどれだけ起動時間を食っているかを見ます。重めの標準ライブラリ 10 個を import して ok を表示するだけのスクリプト imports10.py と、同じ 10 個に lazy を付けた imports10_lazy.py、比較の基準になる empty.py(print("ok") だけ)を用意します。
# imports10.py — 3.14 でも 3.15 でも動く。10 個とも使わない
import json
import re
import email.message
import urllib.request
import http.client
import decimal
import argparse
import dataclasses
import typing
import asyncio
print("ok")
# imports10_lazy.py — 3.15 専用(lazy は 3.14 では SyntaxError)
lazy import json
lazy import re
lazy import email.message
lazy import urllib.request
lazy import http.client
lazy import decimal
lazy import argparse
lazy import dataclasses
lazy import typing
lazy import asyncio
print("ok")
起動時間は、別の Python から子プロセスとして起動して time.perf_counter() で測るのが手軽です。-I を付けると site の読み込みなど環境の差を省けます。7 回起動した最小値を取ります。
py -V:3.14 -I imports10.py
py -V:3.15 -I imports10.py
py -V:3.15 -I imports10_lazy.py
py -V:3.15 -I -X lazy_imports=all imports10.py
| 条件 | 3.14.8 | 3.15.0 |
|---|---|---|
empty.py(print のみ) |
0.030 s | 0.029 s |
imports10.py(通常の import) |
0.193 s | 0.186 s |
imports10_lazy.py(lazy import) |
SyntaxError | 0.029 s |
imports10.py + -X lazy_imports=all |
― | 0.029 s |
使わない import を lazy にすると、起動は空のスクリプトと同じになりました。import に払っていた約 0.16 秒がそのまま消えた形です。読み込みは使った時点で起きるので、合計の時間が減るのは「使わない経路があるとき」に限ります。-X lazy_imports=all を付けると、コードを変えずに同じ結果になりました。
どの import が重いかは -X importtime で見ます。import したモジュールごとの所要時間(自分の分と、入れ子を含む累積)が標準エラー出力に出ます。3.14 からは -X importtime=2 で、すでに読み込み済みだったモジュールも cached として表示されます。
py -V:3.14 -X importtime imports10.py 2> imp314.txt
py -V:3.15 -X importtime imports10.py 2> imp315.txt
出力の最後の行に近いほど外側の import で、累積時間(マイクロ秒)が右の列に出ます。トップレベルの 10 モジュールの累積を足し合わせると、3.14.8 が 176,085 µs、3.15.0 が 167,883 µs で、同じ 10 モジュールでも 3.15 のほうが約 5% 短くなっていました。lazy 版で同じことをすると、使わなかったモジュールの行は出ません。
5-5. インタプリタの速さを純 Python で測る
tail-calling インタプリタの効き目は、負荷の種類で変わります。次の 3 種類を各 5 回走らせた最小値です。JIT は有効にしていません(既定で無効)。
# bench.py — 3 種類の純 Python 負荷
import time
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
def loop():
s = 0
for i in range(3_000_000):
s += i * 2 % 7
return s
def dict_str():
d = {}
for i in range(300_000):
d[f"key{i}"] = f"{i:08d}"
return len(d)
for name, fn in [("fib(27)", lambda: fib(27)), ("loop 3M", loop), ("dict/str 300k", dict_str)]:
best = float("inf")
for _ in range(5): # 5 回走らせた最小値
t = time.perf_counter()
fn()
best = min(best, time.perf_counter() - t)
print(f"{name:14s} {best:.3f} s")
| ベンチ | 3.14.8 | 3.15.0 | 比 |
|---|---|---|---|
| fib(27)(再帰呼び出し) | 0.026 s | 0.015 s | 0.58 倍 |
| 3M 回のループ(整数演算) | 0.168 s | 0.162 s | 0.96 倍 |
| dict/str 300k(文字列整形と辞書) | 0.068 s | 0.071 s | 1.04 倍 |
関数呼び出しが支配的な fib は大きく縮み、整数ループと辞書・文字列の処理はほぼ同じでした。公式の「pyperformance 幾何平均で 15〜20%(Ryzen 7 5800X)」はいろいろな負荷の混合なので、この 3 種で代表はできません。言えるのは、呼び出しが多いコードほど効く というこの範囲での傾向までです。
5-6. 以前の挙動に戻す・新機能を一度動かす
rem このウィンドウだけ、3.14 以前の挙動に戻す
set PYTHONUTF8=0
py -V:3.15 cp932_probe.py
set PYTHONUTF8=
rem そのプロセスだけ戻す
py -V:3.15 -X utf8=0 cp932_probe.py
rem JIT が同梱されているか・有効か
py -V:3.15 -c "import sys; print(sys._jit.is_available(), sys._jit.is_enabled())"
set PYTHON_JIT=1
py -V:3.15 -c "import sys; print(sys._jit.is_available(), sys._jit.is_enabled())"
py -V:3.15 bench.py
set PYTHON_JIT=
rem 新しい組み込み型
py -V:3.15 -c "print(type(frozendict({'a': 1})), sentinel('MISSING'))"
rem Tachyon でサンプリングプロファイル(run / attach / dump / replay)
py -V:3.15 -m profiling.sampling run bench.py
JIT は、3.14.8・3.15.0 とも Windows x64 の embeddable 版で sys._jit.is_available() が True(同梱されている)、既定の is_enabled() は False でした。環境変数 PYTHON_JIT=1 を付けると、3.15.0 では True / True になります。有効にしたときの §5-5 のベンチは次のとおりです(3.15.0・各 5 回の最小値・同じ PC で連続計測。§5-5 とは別の回なので、既定側の値も少し違います)。
| ベンチ(3.15.0) | JIT 無効(既定) | PYTHON_JIT=1 |
|---|---|---|
| fib(27) | 0.014 s | 0.014 s |
| 3M 回のループ | 0.160 s | 0.169 s |
| dict/str 300k | 0.062 s | 0.053 s |
JIT は実験的な機能で既定は無効です。この PC のこの 3 種では、fib は変わらず、ループは 0.160 秒から 0.169 秒、dict/str は 0.062 秒から 0.053 秒と、効き目は一方向ではありませんでした。公式の 7〜8%・11〜12% は Linux と macOS の pyperformance 幾何平均なので、この結果と比べるものではありません。
frozendict({'a': 1}) は <class 'frozendict'>、sentinel('MISSING') の表示は MISSING でした。Tachyon は、fib(26) を 3 回呼ぶだけのスクリプトを run で動かすと Captured 35 samples in 0.04 seconds・Sample rate: 989.84 samples/sec と出て、fib が 100% の hot spot として表示されました。コードに一切手を入れずに、どの関数に時間が集まっているかが見える道具です。
⚠️ 注意点
- macOS 27.0 の IDLE / tkinter:リリースページは、macOS 27.0 で IDLE などの Tk を使う GUI アプリケーションが、ダイアログを開くメニュー操作で固まる(強制終了が必要になる)問題を注記しています。原因は macOS 27.0 側の挙動変更で、現行のすべての Tk と、すべての現行 Python に影響すると見られています。Tk ベースのアプリに依存している場合は、回避策が出るまで macOS 27.0 の導入を見送るか、自分の作業手順に影響がないか確かめることが勧められています。追跡先は CPython の issue #158053 です
- 速度の数字は条件つき:JIT の 7〜8% と 11〜12% は pyperformance の幾何平均で、プラットフォームと比較対象(標準インタプリタか tail-calling か)が異なります。個別のベンチマークでは約 15% の低下から 100% 超の向上まで幅があります。Windows の 15〜20% は AMD Ryzen 7 5800X・Visual Studio 18.1.1 での値です
EncodingWarningは opt-in:既定では出ません。-X warn_default_encodingかPYTHONWARNDEFAULTENCODING=1を付けたときだけ出ます- UTF-8 モードは起動時にしか切り替えられません:
sys.flags.utf8_modeで読めますが、実行中に変えることはできません - 3.14 以前でも動かすスクリプト:
lazyキーワードは 3.14 以前でSyntaxErrorになります。両方で動かすなら__lazy_modules__を使います
当サイトの Python スクリプトを見返すと、cp932 対策の痕跡があちこちにあります。SNS 告知の台帳を作るスクリプトは open() の全箇所に encoding="utf-8" を書き、git を呼ぶ subprocess.run() にも encoding="utf-8" を付けています。KiCad 10 の基板を GUI なしで仕上げるスクリプト群は、先頭で sys.stdout.reconfigure(encoding="utf-8") を呼び、kicad-cli の出力を受ける箇所に errors="replace" まで添えています。Gemini にデータシートを読ませる記事のスクリプトには「cp932 のコンソールに µ などが混ざっても落とさない」というコメントつきで sys.stdout.reconfigure(errors="replace") があり、自作タスクボードの MCP サーバーは Claude Code に登録するとき PYTHONUTF8=1 を環境変数で渡しています。どれも、標準出力がパイプになった瞬間に cp932 へ戻る Windows の挙動に、1 行ずつ対処してきた結果です。
筆者がいちばん変わると受け止めたのは、この「パイプ越しに Python を呼ぶ道具」との相性です。この記事の準備でも、タイトルの文字数を数えるだけの 1 行を 3.11 で走らせたら、呼び出した側に返った日本語が化けました。原因はまさに、リダイレクトされた標準出力が cp932 になることです。3.15 なら起きません。AI のコーディング支援や MCP サーバーのように、人ではなくプログラムが Python の出力を読む場面は増えています。そこで「書き忘れた既定」が UTF-8 になるのは、Windows で道具を作る側にとって地味に大きい変化だと感じます。
ただし、cp932 を吐く相手は残ります。cmd の出力、Excel の CSV、Shift_JIS で送ってくる計測器。3.15 で初めて落ちるのは、こうした相手を encoding 無しで読んでいた箇所で、第 5 節で見た「r.stdout が None」はその典型です。そこには encoding="locale" を明示するつもりです。既定が UTF-8 になっても、PEP 686 の勧めどおり encoding は全部書く、という習慣は変えません。lazy import は、使わない標準ライブラリを 10 個並べた最小のスクリプトで、起動が 0.19 秒から 0.03 秒になるのを見ました。効くのは「使わない経路があるとき」だけなので、次は Claude Code が起動を待つ MCP サーバーと、import が重い KiCad のスクリプトで、どの import が実際に使われずに終わっているかを -X importtime で数えてから付けるつもりです。
✅ まとめ
- Python 3.15.0 は 2026年10月9日に公開された。3.14 からの差分は 5,643 コミット・1,012 人の貢献
- PEP 686 で UTF-8 モードが既定で有効になり、encoding を省いた
open()・subprocessのtext=True・リダイレクトされた標準出力が、日本語 Windows でも UTF-8 になった。コンソール I/O とファイル名は 3.6 からすでに UTF-8 で、変わらない - 影響を受けるのは、cp932 のファイルや
cmdの出力を encoding 無しで読んでいた箇所。直し方はencoding="utf-8"かencoding="locale"の明示で、戻すならPYTHONUTF8=0か-X utf8=0。洗い出しは-X warn_default_encoding - PEP 810 の
lazy importは、読み込みを最初の使用時まで先送りする。import 時の副作用に頼るコードは対象から外し、効果は-X importtimeで自分のスクリプトで測る - Windows x64 の公式バイナリは tail-calling インタプリタになり、pyperformance の幾何平均で 15〜20% 高速(Ryzen 7 5800X・VS 18.1.1)。実験的 JIT は x86-64 Linux 7〜8%・AArch64 macOS 11〜12%
- 手元の Windows 11 で 3.14.8 と 3.15.0 を並べると、encoding 省略の
subprocessはr.stdoutがNoneに、使わない import 10 個の起動は 0.19 秒から 0.03 秒に、fib(27) は 0.026 秒から 0.015 秒になった - 次の一手は、第 5 節の手順で 3.14 と 3.15 を並べ、自分のスクリプトを
-X warn_default_encodingで一度走らせること。3.14 のままなら-X utf8=1で先に試せる
よくある質問(FAQ)
Q: 3.15 に上げると、いまの自作スクリプトは壊れますか?
A: open() や subprocess の encoding を明示しているスクリプトは、何も変わりません。encoding を省いていて、かつ cp932 で保存されたファイルや cmd の出力を読んでいる箇所は、3.15 では復号に失敗します。ファイルの open() は UnicodeDecodeError で止まり、subprocess の text=True は手元の Windows 11 では r.stdout が None になる形で現れました。-X warn_default_encoding を付けて実行すると、該当箇所ごとに EncodingWarning が出るので、そこに encoding="utf-8" か encoding="locale" を書き足します。急ぐときは PYTHONUTF8=0 か -X utf8=0 で以前の挙動に戻せます。移行前に試すなら、3.14 のまま -X utf8=1 を付ければ 3.15 と同じ挙動になります。
Q: encoding="utf-8" と encoding="locale" のどちらを書けばよいですか?
A: 自分で作るファイルや、UTF-8 と分かっているファイル(JSON・TOML・Markdown など)は "utf-8" です。OS の地域設定で保存されたファイル(日本語 Windows なら cp932 の CSV など)や、cmd のように地域設定で出力するコマンドの結果は "locale" です。"locale" は 3.10 から使え、3.11 以降は UTF-8 モードでも地域設定の値を使います。
Q: PYTHONUTF8=0 をシステムの環境変数に入れてよいですか?
A: 公式ドキュメントは勧めていません。システム全体の既定に入れると、3.7 以降のすべての Python アプリケーションに影響します。古い挙動が要るプロセスだけ、そのウィンドウで一時的に設定するか、-X utf8=0 を起動オプションで渡す形が勧められています。
Q: lazy import を全部の import に付ければ速くなりますか?
A: 効くのは、起動時に読み込んでいたのに実際には使わないモジュールがある場合です。import 時の副作用(デコレータでの登録・モンキーパッチ)に頼るモジュールは、先送りすると動作が変わるので対象から外します。まず -X lazy_imports=all で試し、問題のあるモジュールは sys.set_lazy_imports_filter() で除外する順番が PEP 810 の勧めです。効果の数値は CPython 公式には無いので、-X importtime で自分のスクリプトを測ります。
Q: Windows で 3.15 にすると速くなりますか?
A: python.org の Windows 64bit バイナリが tail-calling インタプリタになり、pyperformance の幾何平均で switch-case 方式より 15〜20% 高速とされています(AMD Ryzen 7 5800X・Visual Studio 18.1.1)。これは JIT とは別で、何も設定しなくても効きます。手元の Windows 11 では、再帰呼び出しの fib(27) が 0.026 秒から 0.015 秒に縮み、整数ループと辞書・文字列の処理はほぼ同じでした(第 5 節)。JIT は実験的な機能で、3.14 の What’s New によると PYTHON_JIT=1 で有効にできます。
Q: 3.14 と 3.15 を同じ PC に入れるには?
A: Python install manager を入れて py install 3.14 と py install 3.15 を実行し、py -V:3.14 / py -V:3.15 で版を指定して起動します。従来のフルインストーラ(.exe)は 3.14 から非推奨で、3.16 以降は提供されない予定です。
関連記事
- Claude Code で使える MCP ツールの作り方|Python で自作サーバを1本通す — 標準入出力で JSON をやり取りする Python サーバー。既定エンコーディングの影響を受けやすい形
- 自作 Web アプリに MCP を生やして Claude Code から書き込む|状態を持つ MCP サーバーの作り方 —
PYTHONUTF8=1を環境変数で渡して登録している例 - 【Google AI 実験室 #2】データシート QA|Gemini にデータシート PDF を読ませ、IC実験室の実測値で採点する — cp932 のコンソールで µ を落とさないための
sys.stdout.reconfigure() - N-mixtureモデルで個体数を推定するPythonシミュレータ|25%の観測から全体を推定 — Python で書いたシミュレータ
- サクラエディタ v2.4.3 更新手順とダークモード設定|約3年8か月ぶりの大型更新 — Shift_JIS と UTF-8 が混在するファイルを扱う現場の話
参考
- Python 3.15.0 リリースページ(python.org・2026年10月9日)
- What’s new in Python 3.15(docs.python.org)
- Python 3.15.0 (final) is here!(Python Insider・2026年10月9日)
- PEP 686 – Make UTF-8 mode default
- PEP 810 – Explicit lazy imports
- PEP 597 – Add optional EncodingWarning
- PEP 528 – Change Windows console encoding to UTF-8
- Python UTF-8 Mode(os モジュールのドキュメント)
- Using Python on Windows(UTF-8 mode・Python install manager)
- Command line and environment(-X importtime・-X utf8・-X lazy_imports)
- What’s new in Python 3.14(tail-calling インタプリタ・JIT 同梱の初出)