こんにちは、おうどんです🍜
Mac で書いた Python スクリプトを、Windows のサーバーに置く。 設定ファイルを読むだけの、かわいい処理です。 手元では一発で動いた。
実行する。
UnicodeDecodeError: 'cp932' codec can't decode byte 0x86 in position 11: illegal multibyte sequence
……待って。 cp932 って誰? 呼んでないよ?🤔
説明用の架空の場面ですが、Windows で Python を書いていると、かなりの確率で一度は会う人です。 しかも厄介なことに、この人はエラーを出してくれるときはまだ親切なんです。 本当に怖いのは、エラーを出さずに「エラー」を「繧ィ繝ゥ繝シ」と読んで、そのまま処理を続けるパターン。
今回は、Windows の Python で cp932 が出てくる理由、症状ごとの直し方、書き忘れを機械で見つける方法、そして Python 3.15 で UTF-8 モードが既定になる変更への備えまで、まとめてやってみます。
結論:まずはこのチェックリスト
先に答えを置いておきます。
- テキストファイルの
open()には、ファイルの実際の文字コードに合わせてencoding=を書く(UTF-8 のファイルならencoding="utf-8") Path.read_text()/write_text()にもencoding=を書く- BOM 付き UTF-8 を読むかもしれないなら
encoding="utf-8-sig" - Windows で作られた Shift_JIS 系のファイル(cp932)を扱うなら
encoding="cp932"と明示する subprocess.run(..., text=True)は、子プロセスの出力の文字コードに合わせてencoding=を指定する- 書き忘れは
python -X warn_default_encodingで見つける。CI では-W error::EncodingWarningで落とす - とりあえず全体を UTF-8 にしたいなら UTF-8 モード(
-X utf8/PYTHONUTF8=1) - Python 3.15 からは UTF-8 モードが既定。cp932 のファイルを
encodingなしで読んでいるコードは、逆に壊れる
「毎回書くの、めんどくさくない?」
めんどくさいです。 でも、書かなかった分のツケは、だいたい本番で払います。。。
以下、なぜそうなるのかを確認しながら見ていきます。
なぜ cp932 が出てくるのか:open() の既定は「OSのロケール」
Python 3.14 までの open() は、UTF-8 モードを有効にしていなければ、encoding を省略するとロケールに応じた文字コードを使います(3.15 からの変更は後半で)。 Python 公式の io モジュールのドキュメントでは、TextIOWrapper と open() の既定のエンコーディングはロケール依存で、locale.getencoding() で決まると説明されています。
そして Windows では、このロケールのエンコーディングがいわゆる ANSI コードページです。 Windows で Python を使う公式ガイドにも、Windows はシステムのエンコーディングにレガシーな ANSI コードページを使い、Python はそれをテキストファイルの既定に使う、と書かれています。
システムロケールが日本語の Windows だと、それが cp932 です。 cp932 は Shift_JIS をマイクロソフトが拡張した文字コードで、Shift_JIS とまったく同じではありません(このあと少しだけ触れます)。
実際に確認してみます。
# enc.py
import sys, locale
print("utf8_mode:", sys.flags.utf8_mode)
print("getencoding:", locale.getencoding())
print("getpreferredencoding:", locale.getpreferredencoding(False))
print("filesystem:", sys.getfilesystemencoding())
今回の検証環境(日本語 Windows 10 + Python 3.13.1、UTF-8 モード無効)での結果です。
utf8_mode: 0
getencoding: cp932
getpreferredencoding: cp932
filesystem: utf-8
getencoding が cp932。 つまり open("menu.txt") は「このファイルは cp932 です」という前提で読みにいきます。
一方、filesystem は utf-8 になっています。 ファイル名は UTF-8 で扱うのに、ファイルの中身は cp932 で読む。 ここがややこしいところなんですよね。 (この「名前と中身で担当が違う」話は、note 版で深掘りしています。)
症状1:UTF-8のファイルを読むと UnicodeDecodeError
いちばんよく見る症状です。 UTF-8 で保存されたファイルを、encoding なしで読みます。
# t1.py
from pathlib import Path
Path("menu.txt").write_bytes("きつねうどん 480円\n".encode("utf-8"))
with open("menu.txt") as f:
print(repr(f.read()))
UnicodeDecodeError: 'cp932' codec can't decode byte 0x86 in position 11: illegal multibyte sequence
(トレースバックの最終行のみ)
UTF-8 のバイト列を cp932 として解釈しようとして、cp932 として成り立たない並びにぶつかった、ということです。
直し方はシンプルで、encoding="utf-8" を書くだけ。
with open("menu.txt", encoding="utf-8") as f:
print(repr(f.read()))
'きつねうどん 480円\n'
はい、きつねうどんが帰ってきました🍜
症状2:エラーが出ないまま文字化けする(いちばん怖い)
ここからが本題です。
UTF-8 のバイト列が、たまたま cp932 としても読めてしまうことがあります。 その場合、例外は出ません。 化けた文字列のまま、処理が普通に進みます。
いくつかの単語で試してみました。 UTF-8 でエンコードしたバイト列を、cp932 でデコードしています。
# t3.py
for s in ["うどん", "設定", "テスト", "ログ", "ファイル", "エラー", "成功"]:
b = s.encode("utf-8")
try:
print(s, "->", repr(b.decode("cp932")))
except UnicodeDecodeError as e:
print(s, "-> error", e.reason)
うどん -> error illegal multibyte sequence
設定 -> error incomplete multibyte sequence
テスト -> error illegal multibyte sequence
ログ -> '繝ュ繧ー'
ファイル -> '繝輔ぃ繧、繝ォ'
エラー -> '繧ィ繝ゥ繝シ'
成功 -> '謌仙粥'
「うどん」はエラーになるのに、「エラー」はエラーにならない。 ……なんで「エラー」だけ空気読まないの?
これが何を意味するかというと、たとえばこんな事故です(説明用の架空の例)。
- ログファイルを読んで「エラー」を含む行を数える → 化けているので 0 件。監視は平和。
- CSV の「成功」列でフィルタする → 一致しない。全件が失敗扱い。
どちらも例外は出ません。 テストデータがたまたま化けても読める文字だけだと、手元の確認もすり抜けます。
エラーが出ないことは、正しく読めた証拠にならない。 だからこそ、encoding は「動いたからいいや」ではなく、ファイルの実際の文字コードを最初から書いておくわけです。
症状3:書き込みで UnicodeEncodeError(絵文字・機種依存でない文字)
読み込みだけではありません。 encoding なしで書くと、cp932 で書き込もうとします。 cp932 にない文字があると、今度は書き込みで止まります。
# t4.py
with open("out.txt", "w") as f:
f.write("本日のおすすめ🍜\n")
UnicodeEncodeError: 'cp932' codec can't encode character '\U0001f35c' in position 7: illegal multibyte sequence
(トレースバックの最終行のみ)
🍜 が書けない。 おうどん日和として、これは看過できません。
さらに厄介なのが、書けてしまった場合です。
# t5.py
with open("log.txt", "w") as f:
f.write("処理成功\n")
print(open("log.txt", "rb").read())
print(open("log.txt", encoding="utf-8").read())
b'\x8f\x88\x97\x9d\x90\xac\x8c\xf7\r\n'
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8f in position 0: invalid start byte
(2 行目はトレースバックの最終行のみ)
ファイルの中身は cp932 のバイト列です(改行も \r\n)。 これを UTF-8 前提の別のツールや、Linux 側の処理に渡すと、今度は向こうで失敗します。 自分の環境では何も起きず、受け取った人が困る。 いちばん気まずいタイプです。。。
書き込みも、受け取る側に合わせて encoding= を明示しましょう。 UTF-8 で渡すなら encoding="utf-8" です。
with open("out.txt", "w", encoding="utf-8") as f:
f.write("本日のおすすめ🍜\n")
症状4:subprocess の text=True でも同じことが起きる
見落としがちなのが subprocess です。 subprocess の公式ドキュメントでは、text=True(universal_newlines=True)で encoding を指定しない場合、io.TextIOWrapper の既定、つまりロケールのエンコーディングで開くと説明されています。
UTF-8 を出力する子プロセスを、text=True だけで受け取ってみます。
# child.py(UTF-8 のバイト列をそのまま出力する子プロセス)
import sys
sys.stdout.buffer.write("エラー\n".encode("utf-8"))
# t9.py
import subprocess, sys
child = [sys.executable, "child.py"]
r = subprocess.run(child, capture_output=True, text=True)
print("text=True :", ascii(r.stdout))
r = subprocess.run(child, capture_output=True, encoding="utf-8")
print("encoding='utf-8' :", ascii(r.stdout))
text=True : '\u7e67\uff68\u7e5d\uff69\u7e5d\uff7c\n'
encoding='utf-8' : '\u30a8\u30e9\u30fc\n'
……え、暗号?
ascii() は ASCII 以外の文字を \u のエスケープ表記で出すので、こうなります。 エスケープを文字に戻すと、こうです(ここは実行結果そのものではなく、読みやすく直したものです)。
text=True : '繧ィ繝ゥ繝シ\n'
encoding='utf-8' : 'エラー\n'
text=True だけだと、例外なしで化けています。
ここで print(repr(...)) ではなく ascii() を使っているのには理由があります。 画面に表示する側の文字コードも絡むので、表示を見ただけでは「どこで化けたか」が分からないからです。 文字化けを調べるときは、ascii() でコードポイントを見るか、.encode("utf-8").hex() でバイト列を見るのが確実です。
subprocess では、子プロセスが何の文字コードで出力するかに合わせて encoding= を指定しましょう。 子プロセスが UTF-8 を出すなら encoding="utf-8"、cp932 を出すなら encoding="cp932" です。 「どっちか分からない」なら、まずは text=True を外してバイト列で受け取り、中身を確かめてから決めるのが安全です。
対処:ケース別の書き方
UTF-8 のファイル(いちばん多い)
from pathlib import Path
with open("config.json", encoding="utf-8") as f:
text = f.read()
text = Path("config.json").read_text(encoding="utf-8")
Path("result.txt").write_text("完了🍜\n", encoding="utf-8")
pathlib の read_text() / write_text() も、encoding を省略すればロケール依存です。 open() だけ直して Path を忘れる、はあるあるです。
BOM 付き UTF-8(Windows のツールで保存されたファイルなど)
先頭に BOM(EF BB BF)が付いた UTF-8 ファイルを utf-8 で読むと、BOM が \ufeff という目に見えない文字として残ります。
# t6.py
from pathlib import Path
Path("bom.csv").write_bytes("\ufeffname,price\nきつね,480\n".encode("utf-8"))
with open("bom.csv", encoding="utf-8") as f:
print(repr(f.readline()))
with open("bom.csv", encoding="utf-8-sig") as f:
print(repr(f.readline()))
'\ufeffname,price\n'
'name,price\n'
1 行目の列名が name ではなく \ufeffname になるので、CSV の列名で引くと「そんな列ないよ?」になります。 print() でそのまま表示すると見た目では分からないのが、また意地悪なんですよね。 repr() なら上のように \ufeff が見えるので、怪しいときは repr で確認です。
codecs の公式ドキュメントによると、utf-8-sig はデコード時に先頭の BOM があれば読み飛ばし、エンコード時は先頭に BOM を付けます。 BOM があってもなくても読めるので、読む側は utf-8-sig にしておくと安全です。 書く側で utf-8-sig を使うのは、受け取り側が BOM 付きを求めるときだけにしましょう。
with open("plain.csv", "w", encoding="utf-8-sig", newline="") as f:
f.write("name\n")
b'\xef\xbb\xbfname\n'
本当に cp932 のファイル(Windows の Shift_JIS 系)
古いシステムの出力など、本当に cp932 のファイルもあります。 その場合は「既定でたまたま cp932 だから読めていた」ではなく、encoding="cp932" と明示します。
with open("legacy.csv", encoding="cp932") as f:
text = f.read()
「今の環境のロケールで読みたい」という意図がはっきりしているなら、Python 3.10 以降は encoding="locale" も使えます(io モジュールのドキュメント)。 ただし、ファイルが cp932 だと分かっているなら cp932 と書いたほうが、別の環境に持っていったときに意図が伝わります。
ちなみに、cp932 と shift_jis は Python では別のコーデックです。 cp932 は Shift_JIS にマイクロソフトが文字を足したもので、たとえば「①」は cp932 では書けても、shift_jis では書けません。
# cpsj.py
for ch in ["①", "~"]:
for enc in ("cp932", "shift_jis"):
try:
print(ascii(ch), enc, ch.encode(enc).hex(" "))
except UnicodeEncodeError:
print(ascii(ch), enc, "NG")
print(ascii(b"\x81\x60".decode("cp932")), ascii(b"\x81\x60".decode("shift_jis")))
'\u2460' cp932 87 40
'\u2460' shift_jis NG
'\uff5e' cp932 81 60
'\uff5e' shift_jis NG
'\uff5e' '\u301c'
\u2460 が「①」、\uff5e が全角チルダ「~」、\u301c が波ダッシュ「〜」です。 同じ 81 60 なのに、cp932 は「~」、shift_jis は「〜」と読む。 見た目そっくりで中身は別人。ややこしい。。。
ファイルを作ったシステムが cp932 なのか、狭い意味の Shift_JIS なのかに合わせて指定しましょう。
どちらか分からないファイル
取り込み処理などで、UTF-8 と cp932 が混在していることもあります。 その場合は、バイト列で読んでから順に試す方法があります。
# t10.py
import sys
data = open(sys.argv[1], "rb").read()
for enc in ("utf-8-sig", "cp932"):
try:
print(enc, "->", repr(data.decode(enc)))
break
except UnicodeDecodeError as e:
print(enc, "-> NG:", e.reason, "at", e.start)
cp932 のファイルと、BOM 付き UTF-8 のファイルで試した結果です。
utf-8-sig -> NG: invalid start byte at 0
cp932 -> 'かけ\r\n'
utf-8-sig -> 'name,price\nきつね,480\n'
順番が大事です。 症状2で見たとおり、UTF-8 のバイト列が cp932 として「読めてしまう」ことがあるので、cp932 を先に試すと化けたまま成功扱いになることがあります。 UTF-8 のほうが「違ったらエラーになりやすい」ので先に試す、というのがこの順番の理由です。 とはいえ推測は推測なので、可能なら入力の文字コードを仕様で決めるのがいちばんです。
書き忘れを機械で見つける:EncodingWarning
「encoding を毎回書く」と決めても、人間は忘れます。 おうどんも忘れます。自信を持って忘れます。
そこで Python 3.10 から使える EncodingWarning です(io モジュールのドキュメント、PEP 597 で導入)。 -X warn_default_encoding を付けて実行すると、encoding を省略した場所で警告が出ます。
# t7.py
from pathlib import Path
Path("a.txt").write_text("abc", encoding="utf-8")
with open("a.txt") as f:
f.read()
Path("a.txt").read_text()
python -X warn_default_encoding t7.py
...\t7.py:3: EncodingWarning: 'encoding' argument not specified
with open("a.txt") as f:
...\t7.py:5: EncodingWarning: 'encoding' argument not specified
Path("a.txt").read_text()
(パスは省略しています)
open() と read_text() の両方を、行番号付きで教えてくれます。 環境変数 PYTHONWARNDEFAULTENCODING=1 でも同じことができます。
CI などで「書き忘れがあったら落とす」にしたいなら、警告をエラーに昇格させます。
python -X warn_default_encoding -W error::EncodingWarning t7.py
Traceback (most recent call last):
File "...\t7.py", line 3, in <module>
with open("a.txt") as f:
~~~~^^^^^^^^^
EncodingWarning: 'encoding' argument not specified
終了コードは 1 でした。 encoding="utf-8" を書いた版では、同じオプションでも普通に終了コード 0 で動きます。
ただし、警告が出るのは実際に実行された行だけです。 テストで通らない分岐にある open() は見つかりません。 テストのカバー範囲と合わせて考えましょう。
UTF-8 モード:全体の既定を UTF-8 にする
「全部の open() を直すのは今すぐは無理。でも UTF-8 のファイルは読みたい」
そんなときの選択肢が UTF-8 モードです。 os モジュールのドキュメントによると、UTF-8 モードでは locale.getpreferredencoding() が 'utf-8' を返し、open() などの既定も UTF-8 になります。 有効にするには -X utf8 オプションか、環境変数 PYTHONUTF8=1 を使います。
python -X utf8 enc.py
utf8_mode: 1
getencoding: cp932
getpreferredencoding: utf-8
filesystem: utf-8
getpreferredencoding が utf-8 に変わりました。 一方で getencoding は cp932 のままです。 locale.getencoding() は Python 3.11 で追加された、UTF-8 モードを無視してロケールのエンコーディングを返す関数だからです(PEP 686 でも説明されています)。
UTF-8 モードで、さっきの症状1のスクリプトを動かすと……
python -X utf8 t1.py
'きつねうどん 480円\n'
読めました。
PowerShell で一時的に有効にするなら、こうです。
$env:PYTHONUTF8 = '1' python enc.py Remove-Item Env:PYTHONUTF8
utf8_mode: 1
getencoding: cp932
getpreferredencoding: utf-8
filesystem: utf-8
stdout: utf-8
(enc.py に stdout の行を足した版を、PYTHONUTF8=1 を設定して実行した結果です)
システム全体に設定するときの注意
Windows 向け公式ガイドでは、PYTHONUTF8=1 を既定の環境変数に追加すると、そのシステム上の Python 3.7 以降のアプリケーションすべてに影響する、と注意しています。 レガシーなシステムエンコーディングに依存するアプリケーションがあるなら、環境変数は一時的に設定するか、-X utf8 を使うことが勧められています。
UTF-8 モードでも ANSI コードページを使いたい場面では、mbcs コーデックが使えます(同ガイドより)。
Python 3.15 の変更点:UTF-8 モードが既定になる
ここが、今このテーマを取り上げた理由です。
PEP 686 は、Python 3.15 で UTF-8 モードを既定で有効にする提案で、ステータスは Final です。 PEP 686 自身も、この変更が主に影響するのは Windows ユーザーだと書いています。
Python 3.15 のリリーススケジュール(PEP 790)では、3.15.0 の正式版は 2026-10-01 予定です。 予定は変わることもあるので、実際のリリースは python.org で確認してください(2026-09-24 に PEP 790 を確認)。
つまり 3.15 以降は、Windows でも open("menu.txt") が UTF-8 で読むようになります。 UTF-8 のファイルを encoding なしで読んでいたコードは、何もしなくても直るわけです。
やったー。
……とは、まだ言えません。
逆に壊れるコード:cp932 のファイルを encoding なしで読んでいる
今まで「既定が cp932 だから」たまたま動いていたコードは、逆に壊れます。 UTF-8 モードで、cp932 のファイルを encoding なしで読んでみます。
# legacy.py
with open("sjis.txt") as f:
print(ascii(f.read()))
python legacy.py
'\u304b\u3051\n'
python -X utf8 legacy.py
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x82 in position 0: invalid start byte
(1 つ目の \u304b\u3051 は「かけ」のエスケープ表記。2 つ目はトレースバックの最終行のみ)
UTF-8 モードなしでは「かけ」と読めていたファイルが、UTF-8 モードでは読めなくなりました。 3.15 に上げたら、昨日まで動いていたバッチが止まる。 そういうことが起こり得ます。
3.15 に上げる前にやること
- 今の Python(3.10 以降)で
-X warn_default_encodingを付けてテストを回す - 警告が出た場所に、ファイルの実際の文字コードで
encoding=を書く(UTF-8 なら"utf-8"、cp932 なら"cp932") subprocessのtext=Trueも同様にencoding=を指定する-X utf8で一度テストを回して、3.15 相当の挙動で壊れないか確認する
どうしてもすぐ直せない場合、PEP 686 では PYTHONUTF8=0 か -X utf8=0 で UTF-8 モードを無効にできるとしています。 ただしこれは一時的な逃げ道です。 encoding を明示しておけば、UTF-8 モードが有効でも無効でも同じ動きになります。 結局いちばん強いのは、書いておくことなんですよね。
補足:画面表示は別の話
Windows 公式ガイドによると、UTF-8 モードが無効でも、Windows ではコンソール入出力(標準入出力を含む)とファイルシステムのエンコーディングは UTF-8 が既定です(PEP 528、PEP 529)。
だから、print("エラー") がコンソールで正しく表示されるからといって、open() も UTF-8 で読むとは限りません。 「表示は大丈夫なのにファイルだけ化ける」のは、担当が違うからです。
ただし、これはコンソールにつながっているときの話です。 sys モジュールの公式ドキュメントによると、Windows でもファイルやパイプへの出力はシステムのロケール(ANSI コードページ)を使います。 実際、今回の検証環境では、出力をパイプで受け取る実行方法だと sys.stdout.encoding が cp932 になり、UTF-8 モードを有効にすると utf-8 になりました。 出力先によって表示用の文字コードが変わることがあるので、文字化けの調査では前述のとおり ascii() やバイト列で確認するのが確実です。
まとめ:チェックリスト(再掲)
冒頭の場面では、呼んでいない cp932 がいきなり出てきました。 でも実は、encoding を書かなかった時点で、ちゃんと呼んでいたんです。 「特に指定がなければ、うちのやり方で」と。
open()/Path.read_text()/write_text()に、ファイルの実際の文字コードでencoding=を書いた- BOM が来るかもしれない入力は
utf-8-sigで読む - cp932 のファイルは
encoding="cp932"と明示した subprocess.run(..., text=True)にencoding=を付けた-X warn_default_encodingでテストを回し、警告ゼロにした-X utf8でもテストが通ることを確認した(3.15 への備え)
encoding を書かないのは、「おまかせで」と注文するのと同じ。 おまかせで出てきたのが cp932 でも、文句は言えないんです。
まずは、いちばんよく触るスクリプトを 1 本だけ、-X warn_default_encoding 付きで動かしてみてください。 警告が 0 件なら、今日はきつねうどんで乾杯です🍜
参考資料
- Python 公式:io — Text Encoding / Opt-in EncodingWarning —
open()の既定がロケール依存であること、encoding="locale"(3.10〜)、-X warn_default_encodingとPYTHONWARNDEFAULTENCODING - Python 公式:Using Python on Windows — UTF-8 mode — Windows で ANSI コードページが既定になる理由、
PYTHONUTF8=1をシステム全体に設定するときの注意、mbcsコーデック、コンソール入出力とファイルシステムは UTF-8 が既定であること - Python 公式:os — Python UTF-8 Mode — UTF-8 モードで変わる項目と有効化の方法(ドキュメント版 3.14)
- PEP 686 – Make UTF-8 mode default — Python 3.15 で UTF-8 モードを既定にする決定(Final)、
PYTHONUTF8=0/-X utf8=0での無効化、locale.getencoding() - PEP 597 – Add optional EncodingWarning —
EncodingWarning(Python 3.10)の導入 - PEP 528 / PEP 529 — Windows のコンソール入出力・ファイルシステムのエンコーディングを UTF-8 にした変更(Python 3.6)
- PEP 790 – Python 3.15 Release Schedule — 3.15.0 正式版の予定日(2026-10-01)
- Python 公式:sys.stdout — Windows では UTF-8 はコンソールのときだけで、ファイルやパイプは ANSI コードページを使うこと
- Python 公式:subprocess —
text=Trueでencoding未指定時はio.TextIOWrapperの既定を使うこと - Python 公式:codecs — utf-8-sig — BOM の読み飛ばしと付与
確認日:2026-09-24。

コメント