2026 年 9 月 1 日更新: 全面的に書き直しました。初版の「print が化けるのはコンソールが CP932 だから、chcp 65001 で直る」は誤りです。16 通りの実測で、chcp は Python の出力エンコーディングを 1 度も変えませんでした(5 章)。撤回した記述 5 件と未検証項目は 11 章に。
1. 症状から原因を特定する — エラー原文別 早見表
文字コードの事故は例外で止まるものと止まらずに壊れるものに分かれます。怖いのは後者で、化けたまま書かれた CSV は誰も気づかず後工程へ流れます。以下のエラー原文はすべて自宅検証機(Windows 11 Pro 26200.9168 / Python 3.12.10)で再現させて採取しました。
| 症状・エラー原文 | 起きていること | 直す |
|---|---|---|
UnicodeDecodeError: 'cp932' codec can't decode byte 0x82 in position 5: illegal multibyte sequence |
UTF-8 のファイルを cp932 として読んだ。encoding 未指定の open() が多い |
encoding="utf-8" を明示(3 章) |
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x93 in position 0: invalid start byte |
逆方向。cp932 のファイルを UTF-8 として読んだ | encoding="cp932"(4 章) |
UnicodeEncodeError: 'cp932' codec can't encode character '\U0001f600' in position 11: illegal multibyte sequence |
cp932 に無い文字(絵文字など)を書こうとした | 出力先を UTF-8 に。落とせない場面は errors=(3 章) |
コンソールでは出るのに > out.txt やタスクスケジューラ経由だと上の UnicodeEncodeError で落ちる |
コンソールとファイルで出力エンコーディングが別(UTF-8 と cp932) | reconfigure() か UTF-8 モード(5 章) |
chcp 65001 を打っても何も変わらない |
正常。chcp は Python の sys.stdout.encoding を変えない |
5 章(chcp では直らない) |
例外は出ないが 貂ゥ蠎ヲ のような文字列になる |
UTF-8 のバイト列が cp932 として読めてしまった | 読み側の encoding を直す(2 章) |
CSV の日本語が全部 ? になった |
ASCII で書き出された。PowerShell 5.1 の Export-Csv の既定。復元不可 |
Export-Csv -Encoding UTF8 を付ける(5.1 の UTF8 は BOM 付き=公式記述。詳細は別記事・12 月公開) |
subprocess の stdout が None なのに returncode は 0 |
UTF-8 モード下で cp932 出力を decode できず、例外が裏スレッドで潰された | encoding="cp932" を明示(8 章) |
'・ソ@echo' は、内部コマンドまたは外部コマンド…として認識されていません。 |
.bat を UTF-8 BOM 付きで保存した。BOM が 1 行目に混ざった |
BOM なしで保存し直す(別記事・12 月公開) |
文字列に終端記号 " がありません。(TerminatorExpectedAtEndOfString) |
日本語を含む .ps1 を BOM なしで保存した。.bat と逆 |
BOM 付き UTF-8 で保存し直す(別記事・12 月公開) |
ZIP を展開するとファイル名が ë╖ôxç@.txt になる |
ZIP のヘッダに UTF-8 フラグが無く、名前が cp932 のまま | ZipFile(z, metadata_encoding="cp932")(3 章) |
| matplotlib のグラフで日本語が □ になる | 文字コードの問題ではありません。フォントに字形が無いだけ | 品質管理グラフの記事へ |
最後の 1 行は毎回混同されます。バイト列の解釈がずれるのが文字コード問題、字形が無いのがフォント問題で、直すファイルも別です。
position 1234 と言われてもその位置に何があるかは分かりませんが、例外オブジェクトが元データを持っています。
try:
text = path.read_text(encoding="cp932")
except UnicodeDecodeError as e:
print(e.encoding, e.start, e.reason) # cp932 5 illegal multibyte sequence
around = e.object[max(0, e.start - 10): e.start + 10]
print("utf-8なら:", around.decode("utf-8", errors="replace"))
実測では周辺バイトを UTF-8 として読み直すと 日時,値,備� となり、正解が UTF-8 であることがその場で分かりました。UnicodeEncodeError なら e.object[e.start:e.end] が原因の 1 文字です。
2. 壊れているのは文字ではなくバイト列の解釈
Python 3 の文字列は内部で Unicode です。壊れるのは OS と接する境界でバイト列に変換する瞬間だけ——ファイル I/O、標準入出力(5 章)、サブプロセス(8 章)、外部から来たバイト列(シリアル通信・TCP/IP 通信)の 4 か所です。
重要なのは cp932 側が例外を出さないことです。逆に言えば化けた文字列は元のバイト列を保っているので戻せる場合があります。"貂ゥ蠎ヲ".encode("cp932").decode("utf-8") で 温度 が返れば、読み側の encoding を直すだけで済みます。
2.1 cp932 / shift_jis / mbcs / oem の使い分け
| コーデック名 | 実体 | 使いどころ |
|---|---|---|
cp932(別名 932 / ms932 / windows-31j) | Shift_JIS + NEC 特殊文字・IBM 拡張漢字 | これを使う |
shift_jis(別名 sjis) | JIS X 0208 ベース。Microsoft 拡張なし | 使わない。①や㈱で失敗する |
mbcs(別名 ansi) | 実行中の PC の ANSI コードページを指す別名 | PC 設定で中身が変わる。配布アプリでは避ける |
oem | OEM コードページ(3.6 追加) | コンソールアプリの出力を読むとき(8 章) |
混乱の出所は名前の重複です。Microsoft のコードページ一覧では 932 の .NET 名が shift_jis と定義され、Python が cp932 と呼ぶものを Microsoft は shift_jis と呼びます。同じ 932 に名前が 3 つあるのが「Shift_JIS を指定したのに読めない」の原因です。
2.2 cp932 に書ける文字・書けない文字
「機種依存文字は危ない」は基準になりません。cp932 にその文字があるかだけが基準です。1 文字ずつ encode() して確かめました。
| コーデック | 書ける | 書けない(UnicodeEncodeError) |
|---|---|---|
cp932 | ① ㈱ 〜 ~ 髙 﨑 ℡ № Ⅰ Ⅱ ≒ ∵ ㎡ ㈲ ° Ω ± | ♬ 🄫 😀 🌡 |
shift_jis | 〜 ≒ ∵ ° Ω ± | ① ㈱ ~ 髙 﨑 ℡ № Ⅰ Ⅱ ♬ ㎡ ㈲ 🄫 😀 🌡 |
いわゆる「はしごだか」の 髙 も 﨑 も cp932 で問題なく書けます(b'\xee\xe0\xed\x95')。落ちるのは絵文字と ♬ のような Unicode 側にしか無い記号です。
2.3 波ダッシュ問題 — エラーが出ないのに文字が変わる
もっとも見つけにくいのがこれです。同じバイト列 0x81 0x60 が、コーデックによって別の Unicode 文字になります。
>>> b"\x81\x60".decode("cp932")
'~' # U+FF5E FULLWIDTH TILDE
>>> b"\x81\x60".decode("shift_jis")
'〜' # U+301C WAVE DASH
これは環境差ではなくコーデックの仕様で、Microsoft 提供の CP932 マッピング表に 0x8160 → 0xFF5E FULLWIDTH TILDE と定義されています。どちらも例外を出さずに成功するため、「読めているのに後工程で文字列比較が一致しない」形で表面化します。書き込み方向はさらに非対称でした。
| 操作 | cp932 | shift_jis |
|---|---|---|
b"\x81\x60" を decode | ~(U+FF5E) | 〜(U+301C) |
"〜"(U+301C)を encode | 成功 b'\x81`' | 成功 b'\x81`' |
"~"(U+FF5E)を encode | 成功 b'\x81`' | UnicodeEncodeError |
つまり cp932 で読んだ文字列を shift_jis で書き戻すと、その 1 文字で落ちます。読みと書きで同じコーデックを使うのが最短の防御です。
3. 読む・書く — encoding と errors= の選び方
対策の 9 割は 1 行です。open() に必ず encoding を書く。Python 3.14 以前の Windows では、省略すると locale.getencoding()(日本語版なら cp932)が使われ、同じコードが PC ごとに違う結果を出します。Microsoft 自身も 「ANSI コードページは PC ごとに異なることがあり、1 台の PC でも変更できるため、データ破損につながる」と警告しています。ログハンドラも同様で、渡し忘れると書けない文字が現れた行が丸ごと欠落します(logging 設計の記事)。
import logging.handlers # logging だけでは handlers を参照できない
# 良い例 — どの PC でも同じ結果になる
with open("data.csv", "r", encoding="utf-8") as f:
text = f.read()
handler = logging.handlers.TimedRotatingFileHandler(
"logs/app.log", when="midnight", backupCount=14,
encoding="utf-8", # 未指定だと日本語 Windows は cp932
errors="backslashreplace") # 書けない文字で行を落とさない
3.1 errors= — どれを選ぶと何が失われるか
errors= は「例外を止めるスイッチ」ではなくどう壊すかの選択です。温度 ① ㈱ 髙 😀 25.3 を cp932 に書き出した実測結果です。
errors= | 書き込み(encode)の結果 | 元に戻せるか |
|---|---|---|
strict(既定) | UnicodeEncodeError で停止 | — |
ignore | 温度 ① ㈱ 髙 25.3(消える) | 不可 |
replace | 温度 ① ㈱ 髙 ? 25.3 | 不可 |
backslashreplace | 温度 ① ㈱ 髙 \U0001f600 25.3 | 目視で追える |
xmlcharrefreplace | 温度 ① ㈱ 髙 😀 25.3 | 目視で追える |
namereplace | 温度 ① ㈱ 髙 \N{GRINNING FACE} 25.3 | 目視で追える |
読み込み側は replace が �(U+FFFD)を入れます。encode 側は ?、decode 側は � という非対称は診断に使えます。復元可能性も実測し、読んで書き戻して元と一致したのは surrogateescape だけでした。
- ログ・画面出力 —
backslashreplace。行を落とさず、何が書けなかったか残る - 読んで書き戻す中継処理 —
surrogateescape。バイト列がそのまま保たれる - 人が読む最終成果物 —
strictのまま落とす。壊れた CSV を黙って納品するより安全 ignoreは使わない — 消えた事実すら残らない
3.2 ファイル名・パスは別枠(PEP 529)
「日本語のファイル名が化ける」は中身とは別の仕組みです。Python 3.6 の PEP 529 以降、Windows のファイルシステムエンコーディングは utf-8 + surrogatepass です。自宅検証機では 温度①テスト フォルダも、cp932 に無い絵文字を含む 髙﨑_😀.txt も追加設定なしで作成・列挙できました。今の Python でファイル名が化けるなら原因は外側で、多いのが ZIP です。
zipfile.ZipFile("archive.zip").namelist()
# -> ['ë╖ôxç@.txt']
zipfile.ZipFile("archive.zip", metadata_encoding="cp932").namelist()
# -> ['温度①.txt']
metadata_encoding(3.11 追加)は読み取り専用で、書き込み時に渡すと ValueError: metadata_encoding is only supported for reading files。ヘッダの UTF-8 フラグがあるとそちらが優先されます(実測で flag_bits 2048 の書庫では cp932 指定が無視されました)。書き出す ZIP の名前は ASCII か UTF-8 のみです。
4. 外部から来た CSV — 自動判定は当たらない
取引先や既存システムから届く CSV は UTF-8(BOM あり/なし)と cp932 のどれでもありえます。多くの記事が「判定関数を作る」か「判定ライブラリを使う」を勧めますが、今回の実測では両方が外れました。確実なのは判定ではなく送り元との取り決めです。よくある実装は「先頭 4096 バイトを UTF-8 として decode してみて、通れば UTF-8」ですが、先頭に日本語が無いファイルで必ず外れます。200 行の ASCII 行の最後の 1 行にだけ日本語を入れた 7,553 バイトの cp932 CSV で試しました。
detect_encoding(path) # -> 'utf-8'(誤判定。先頭 4096 バイトが全部 ASCII のため)
path.read_text(encoding="utf-8")
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 7534: invalid start byte
設備のロガーが吐く CSV はまさにこの形(タイムスタンプは ASCII、備考欄にだけ日本語)です。誤判定してから position 7534(0 起点)で落ちるため、途中まで処理が進んだ状態で止まります。では定番のライブラリなら——charset-normalizer(自宅検証機は 3.4.7 / MIT。requests の依存で入っていることが多い)で同じファイルを判定させました。
| ファイル(正解) | charset-normalizer 3.4.7 の判定 | 結果 |
|---|---|---|
| 普通の cp932 CSV(1 行目から日本語) | cp949 | 外れ |
| 先頭 4096 バイトが ASCII の cp932 CSV | cp1252 | 外れ |
| BOM なし UTF-8 CSV | utf_8 | 正解 |
| BOM 付き UTF-8 CSV | utf_8 | 正解 |
1 行目の cp949(韓国語)がとくに危険です。cp949 は cp932 とバイト範囲が重なるため例外を出さずに読めてしまい、実測では 日時,設備,温度 が 볷렄,먠뷈,돴뱗 になりました。例外なしで全文が別の言語に変わるので下流まで誰も気づきません。処理時間は 2.88 MB で自作判定 0.0001 秒、charset-normalizer 0.204 秒ですが、速いほうも遅いほうも間違えたので速度は選定理由になりません。
現実的な受け入れ方は、BOM という確実な手掛かりだけを使い、残りは候補を順に試して失敗を検知する形です。
# 送り元と取り決めた候補を、優先順に並べる
CANDIDATES = ("utf-8-sig", "utf-8", "cp932")
def read_csv_text(path: Path) -> tuple[str, str]:
"""テキストと、実際に成功したエンコーディング名を返す。"""
if path.read_bytes()[:3] == b"\xef\xbb\xbf": # BOM は確実な手掛かり
return path.read_text(encoding="utf-8-sig"), "utf-8-sig"
for enc in CANDIDATES:
try:
# ファイル全体を試す。先頭だけ見ると上の罠を踏む
return path.read_text(encoding=enc), enc
except UnicodeDecodeError:
continue
raise ValueError(f"{path.name}: 候補 {CANDIDATES} のどれでも読めません")
ファイル全体を試すので誤判定が起きず、成功したエンコーディング名を返すのでログに残せば「いつ送り元の仕様が変わったか」を追えます。cp932 は多くのバイト列を受け付けるので候補の最後に置いてください。先に試すと UTF-8 を化けたまま「成功」と判定します。
5. 画面に出すか、ファイルに落とすか — 手元で再現しない理由
ここが本記事の中心です。Windows では、同じ print() でも出力先でエンコーディングが変わります。公式の sys.stdout の項に明記されています。
"On Windows, UTF-8 is used for the console device. Non-character devices such as disk files and pipes use the system locale encoding (i.e. the ANSI codepage)."
(Windows では、コンソールデバイスには UTF-8 が使われる。ディスクファイルやパイプのような非キャラクタデバイスは、システムロケールのエンコーディング=ANSI コードページを使う)
Python 3.6 の PEP 528 で、コンソールへの入出力が ANSI API から Unicode API 経由に変わりました。コンソールに書く Python はコードページを経由せず Unicode を渡すため、chcp の値は関係ありません。リダイレクトやパイプでは従来の経路が使われ、ANSI コードページ(cp932)で encode されます。
5.1 実測 — chcp は 1 度も効かなかった
コンソールを確保し、出力先・コードページ・環境変数を変えながら sys.stdout.encoding と print("温度 ① ㈱ 〜 髙 😀") の成否を記録しました。コードページは chcp と Win32 API の SetConsoleOutputCP で切り替え、子プロセス側で GetConsoleOutputCP() を読んで切り替わりを確認しています。実測した 16 通りのうち、結論に効く 11 通りを次の 10 行にまとめました(残る 5 通りは入力側のコードページも切り替えた場合と .bat から起動した場合で、.bat の中で chcp 65001 を実行してからリダイレクトしても cp932 のままでした)。
| 出力先 | コンソールCP | 環境変数 | isatty() | stdout.encoding | print() |
|---|---|---|---|---|---|
| コンソール | 932 | — | True | utf-8 | 成功 |
| コンソール | 65001 | — | True | utf-8 | 成功 |
| ファイルへリダイレクト | 932 | — | False | cp932 | UnicodeEncodeError |
| ファイルへリダイレクト | 65001 | — | False | cp932 | UnicodeEncodeError |
| パイプ | 932 | — | False | cp932 | UnicodeEncodeError |
| コンソール | 932 | PYTHONIOENCODING=utf-8 | True | utf-8 | 成功 |
| リダイレクト | 932 | PYTHONIOENCODING=utf-8 | False | utf-8 | 成功 |
| コンソール | 932 / 65001 | PYTHONLEGACYWINDOWSSTDIO=1 | True | cp932 | UnicodeEncodeError |
| リダイレクト | 932 | PYTHONUTF8=1 | False | utf-8 | 成功 |
| コンソール | 932 | PYTHONUTF8=1 | True | utf-8 | 成功 |
chcp 65001はsys.stdout.encodingを 1 度も変えませんでした。chcpはコンソールの表示側の設定で、Python の encode 経路には関与しませんPYTHONIOENCODINGはリダイレクト時にだけ効きます。公式にも 「Windows では対話的なコンソールバッファに対してこの変数は無視される(PYTHONLEGACYWINDOWSSTDIOを併せて指定した場合を除く)。標準ストリーム経由でリダイレクトされたファイルやパイプは影響を受けない」とあり、後半の記述は実測と一致しました(併用時の挙動は未実測です)。「設定したのに直らない」の正体ですPYTHONLEGACYWINDOWSSTDIO=1はコンソールでも落ちるようにします。PEP 528 以前に戻す変数なので、意図せず設定されていないか確認を
同じプロセス内で stdout が cp932、stderr が utf-8 になる状態も観測しました。標準出力だけリダイレクトした場合で、エラーは正常に見えるのに通常の出力だけが落ちます。コンソールで動かすうちは問題が見えないので、リダイレクト前提で組むのが正解です。アプリ側で張り替えるのが最も確実で、PYTHONIOENCODING を渡す方法、UTF-8 モード(7 章)が続きます。
if sys.stdout is not None: # exe の --noconsole では None
sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace")
reconfigure()(3.7+)の落とし穴を 2 つ実測しました。encoding だけ渡すと errors が strict にリセットされます(既定の surrogateescape が消えるので errors= も一緒に渡す)。そして読み込み済みのストリームは変更できません——read() 後は UnsupportedOperation になります(書き込み後は可能でした)。
6. Excel と CSV — BOM を付けるかどうか
「人が Excel で開く CSV」だけは UTF-8 に BOM を付けるのが実務的な解です。Microsoft のサポート記事は "You can open a CSV file encoded with UTF-8 normally if it was saved with BOM (Byte Order Mark)." とBOM 付きを前提に案内し、BOM が無い場合は「データ」タブの取り込み機能を案内しています。
# 人が Excel で開く CSV
with open("output.csv", "w", encoding="utf-8-sig", newline="") as f:
csv.writer(f).writerow(["日時", "設備", "温度"])
# システム間連携だけの CSV は BOM なしでよい
with open("data.csv", "w", encoding="utf-8", newline="") as f:
...
newline="" は Excel 対策ではなく csv モジュールの要件で、公式が 「指定しないと引用符で囲まれたフィールド内の改行が正しく解釈されず、余分な \r が追加される」としています。
BOM の実体は先頭 3 バイトの EF BB BF で、付けると読み側にも影響します。utf-8-sig で書いたファイルを utf-8 で読むと '日時,値\n' と先頭に見えない 1 文字が付き、cp932 で読むと UnicodeDecodeError: 'cp932' codec can't decode byte 0xef in position 0 になります(実測)。前者が事故の元で、1 列目のヘッダ名だけが一致しない形で表面化します。読む側を utf-8-sig にしておけば BOM の有無を問わず読めます。
なお「BOM なし UTF-8 をダブルクリックすると化ける」は、自宅検証機に Excel が無いため確認できていません。Microsoft 公式が BOM 付きを前提に案内していることが根拠です。Excel の版や地域設定でも変わるので、納品前に受け取る側の PC で 1 度開いてもらうのが確実です。
7. UTF-8 モードを使う — 効く範囲と効かない範囲
Python 3.7 以降には、I/O の既定エンコーディングをまとめて UTF-8 にする UTF-8 モード(PEP 540)があります。有効にすると encoding の書き漏らしが事故になりません。
set PYTHONUTF8=1 & REM cmd(PowerShell では $env:PYTHONUTF8 = "1")
python -X utf8 script.py # コマンドラインオプション(3.7+)
python -X utf8=0 script.py # 明示的に無効化
恒久化するなら Windows の「環境変数の編集」でユーザー環境変数に登録でき、管理者権限は要りません。
| UTF-8 モードで変わるもの | 変わらないもの |
|---|---|
open() の既定エンコーディング | locale.getencoding()(ロケール本来の値のまま) |
sys.stdin / stdout / stderr | Windows の ANSI コードページそのもの |
locale.getpreferredencoding() が 'utf-8' に | コンソールの chcp(5 章) |
| 引数・環境変数・ファイル名の decode | PowerShell と .bat(別記事) |
subprocess のテキストモード | encoding="locale" を明示した箇所 |
右列が重要です。UTF-8 モードは Python の中だけの設定で、OS もシェルも変えません。現場で化けたときは、推測する前に次を出力させると切り分けが 1 往復で済みます。
print(f"utf8_mode : {sys.flags.utf8_mode}")
print(f"stdout.encoding: {sys.stdout.encoding}")
print(f"stdout.isatty : {sys.stdout.isatty()}") # False ならリダイレクト先
print(f"getencoding : {locale.getencoding()}") # 3.11+ ロケール本来の値
print(f"preferred : {locale.getpreferredencoding(False)}")
最後の 2 つを両方出すのが要点です。locale.getencoding()(3.11 追加)は公式に 「UTF-8 モードを無視する」と明記され、getpreferredencoding(False) は有効時に常に 'utf-8' を返します。自宅検証機では前者が cp932、後者が utf-8 になりました。2 つが食い違えば UTF-8 モードが効いていると判定できます。
8. UTF-8 モードで壊れるもの
UTF-8 モードは万能スイッチではありません。既存の cp932 資産と接する場所で逆向きに壊れます。有効にすると subprocess(..., text=True) の decode も UTF-8 になりますが、Windows のコンソールアプリは cp932 で出力するため読めません。
| 呼び出し方 | PYTHONUTF8 未設定 | PYTHONUTF8=1 |
|---|---|---|
capture_output=True, text=True | '温度 ① 正常\n' | stdout が None・returncode は 0 |
stdout=PIPE, text=True | '温度 ① 正常\n' | UnicodeDecodeError で停止 |
text=True, errors="replace" | '温度 ① 正常\n' | U+FFFD が 8 個に置き換わる |
text=True, encoding="cp932" | '温度 ① 正常\n' | '温度 ① 正常\n' |
text=True, encoding="oem" | '温度 ① 正常\n' | '温度 ① 正常\n' |
1 行目が最悪です。capture_output=True は標準出力と標準エラーを別スレッドで読むため、decode の例外がそのスレッド内で握り潰されます。呼び出し側に見えるのは returncode 0 と stdout is None だけ。標準エラーには Exception in thread Thread-1 (_readerthread) と UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte が出ますが、サービスや GUI アプリなら誰も見ません。errors="replace" も解決になりません——中身が U+FFFD になって復元できなくなります。
正解は encoding の明示です。公式も 「出力データの実際のエンコーディングは呼び出すコマンドに依存するため、テキストへのデコードはアプリケーション側で扱う必要がしばしばある」と警告しています。
# 外部コマンドの出力は「相手が何で吐くか」で決める。UTF-8 モードの有無に左右されない
r = subprocess.run(["cmd", "/c", "dir"],
capture_output=True, text=True,
encoding="oem", # コンソールアプリの出力
errors="backslashreplace") # 想定外の文字で止めない
oem と cp932 の選択は OEM と ANSI のコードページが一致するかで決まります。自宅検証機は両方 932 のため差は出ませんでした。原則はコンソールアプリの出力が oem、テキストファイルが cp932 です。open() も同様に壊れるので、UTF-8 モードを入れる前に書き漏らしを洗い出すのが順序です。
9. Python 3.15 で既定が変わる — 今やっておく 3 ステップ
PEP 686 が Final になり、Python 3.15 から UTF-8 モードが既定になります。3.15.0 の最終リリースは 2026 年 10 月 1 日予定、本記事の更新時点(2026 年 9 月 1 日)はリリース候補 2 の段階です。rc は本番投入するものではありませんし、現場 PC が 3.15 になるのはさらに先です。それでも今やる価値があるのは、3.15 対応がそのまま今の文字化け対策になるからです。オプトアウトも PYTHONUTF8=0 / -X utf8=0 で用意されています。
ステップ 1 — 依存箇所を機械的に洗い出す。encoding を省略した open() を、目視ではなく Python 自身に探させます(EncodingWarning は 3.10 追加)。
python -X warn_default_encoding script.py
# app.py:4: EncodingWarning: 'encoding' argument not specified
# with open(p) as f:
# CI で機械的に落とすなら、警告をエラーに昇格させる
python -X warn_default_encoding -W error::EncodingWarning script.py
ステップ 2 — 見つかった箇所に意図を書く。惰性で全部 utf-8 にすると既存の cp932 ファイルを読んでいた箇所が壊れるので、2 種類に分けます——自分たちが作るファイルは encoding="utf-8"、外部システムが cp932 で吐くファイルは encoding="cp932"。実行 PC のロケールに合わせたい箇所だけ、例外的に encoding="locale"(3.10+)を使います。"locale" は省略と違って意図が読めるので、レビューで「環境依存で正しい」と判断できます。
ステップ 3 — 3.15 を待たずに先行検証する。PYTHONUTF8=1 を付けて今の Python で動かせば、3.15 で起きることの大部分を先に確認できます。ここで 8 章の subprocess 問題が出るはずなので潰しておきます。なお自宅検証機に 3.15 は無いため、3.15 そのものでは検証していません。3.12.10 に -X utf8 を与えた先行確認で、公式が示す移行手順に沿ったものです。
最後に線引きです。UTF-8 既定化は Python の内側だけの変更で、3.15 になっても PowerShell 5.1 のコマンドレット既定・.bat の保存エンコーディング・Excel の CSV の開き方・外部コマンドの出力・Windows の ANSI コードページは変わりません。現場の問題のかなりの部分がここに残ります。
この先の PowerShell と .bat / .ps1 は、Python のコードを 1 行も直さなくても現場 PC を化けさせる領域です。分量が増えたため別記事に分けました——Export-Csv の既定が ASCII で日本語が ? に潰れること、> が UTF-16LE になること、.bat は BOM を付けると壊れ .ps1(PowerShell 5.1)は BOM を付けないと壊れるという真逆のルールまで、同じ自宅検証機の実測で扱っています。
Python 側の対策だけで完結する読者はこのまま 10 章へ、バッチや PowerShell からスクリプトを起動している読者はPowerShell と .bat の文字化け対策(Windows 現場PC編)へどうぞ(2026 年 12 月公開予定)。
10. exe・サービス・タスクスケジューラ
5 章の帰結として、コンソールに繋がらない実行形態はすべてリダイレクト側の挙動になります。公式にも、NUL のような非コンソールのキャラクタデバイスについて 「起動時のコンソール入出力コードページを使う。プロセスが起動時にコンソールに接続されていない場合は、システムロケールのエンコーディングが既定になる」とあります(ディスクファイルとパイプはそもそも ANSI コードページ側です)。タスクスケジューラの「ログオンしているかどうかにかかわらず実行」、Windows サービスや NSSM 経由(常駐化の記事)は、いずれも sys.stdout.encoding が ANSI コードページになります。
加えて --noconsole の exe では sys.stdout が None になることがあり、reconfigure() を無条件に呼ぶと AttributeError で落ちます。冒頭のコードを if sys.stdout is not None: で囲んであるのはこのためです。この形態では標準出力を誰も読まないので、記録はファイルログに寄せます(logging 設計の記事)。
exe の UTF-8 モードはビルド時に焼き込むのが確実で、pyinstaller --python-option "X utf8" app.py と指定します。当サイトの PyInstaller 配布の記事では実行時の PYTHONUTF8=1 が exe には効かなかったと整理していますが、今回は再実測していません。ビルド時指定ならどちらにせよ確実です。
11. おわりに — 事故らせない設計と、撤回した記述
文字コードは、都度直すより境界を 1 か所に閉じ込めるほうが早く終わります。内部処理は UTF-8 で統一し、外部仕様に合わせる変換は入口と出口の関数だけに置く。書き出し側は strict(既定)のままにしておくのが要点で、replace で書くと ? になって復元できません。壊れた CSV を黙って渡すより、止まって気づけるほうが安全です。
チームで決める価値があるのは 4 つ。encoding を省略した open() を禁止する(-X warn_default_encoding で検出できます)。新規に作るファイルは UTF-8、人が Excel で開くものだけ utf-8-sig。受け取るファイルは判定せず送り元と取り決める。ログに自分のエンコーディングを 1 行残す——次の調査が 1 往復で終わります。
| 初版の記述 | 実測で分かったこと |
|---|---|
print が化けるのはコンソールが CP932 だから。chcp 65001 で直る | 誤り。コンソール直では stdout.encoding が utf-8 で落ちない。chcp は 16 通りのどれでも出力エンコーディングを変えなかった(5 章) |
.bat の BOM エラーは '■@echo' になる | 実測は '・ソ@echo'。文字列が違っていた(実測の詳細はPowerShell と .bat の記事) |
ファイル名の日本語が化けるのは古いライブラリが str.encode("ascii") で OS API を呼ぶため | 根拠のない推測だった。PEP 529 以降ファイルシステムは UTF-8 で、絵文字入りの名前も作成・列挙できた(3 章) |
| 外部 CSV は先頭 4096 バイトを見る自作関数で判定する | 先頭が ASCII のファイルで誤判定した。charset-normalizer も cp932 を cp949 と誤判定した(4 章) |
| 波ダッシュの挙動は「筆者の Windows 環境で実測確認」 | 環境差ではなくコーデックの仕様。CP932 のマッピング表に定義がある(2 章) |
検証環境: 自宅検証機(Windows 11 Pro ビルド 26200.9168 / Python 3.12.10 / ANSI・OEM コードページとも 932 / charset-normalizer 3.4.7)での 2026 年 9 月 1 日時点の結果です。未検証は、Python 3.15・Excel(いずれも未導入)、タスクスケジューラと NSSM の実機起動(10 章は公式記述と 5 章の実測からの帰結)、PyInstaller の exe に対する環境変数の効き方です。PowerShell(5.1 のコマンドレット・[Console]::OutputEncoding)と .bat / .ps1 の実測は別記事に移しました(PowerShell と .bat の文字化け対策・2026 年 12 月公開)。
また Windows 11 の「ベータ: ワールドワイド言語サポートで Unicode UTF-8 を使用」設定は意図的に検証していません。プロセス単位ではなくシステム全体の ANSI コードページを変更するもので、再起動が必要なうえ、既存アプリの ZIP・.bat・ファイル名で不具合が出るという報告があります(公式に破損の警告そのものは書かれていません)。稼働中の現場 PC でこの設定を押すのは勧めません。本記事の対策はすべてアプリ側で完結します。
次に読む 1 本は、ここで整えた文字コードのまま現場 PC へ配る話です → PyInstaller で exe 化して配布する(10 章で触れた sys.stdout is None がそのまま次のテーマになります)。
⚠️ 実機適用時の注意: 本記事のコードと数値は自宅検証機での確認に基づくもので、文字コードの設定変更はアプリの動作を保証する仕組みではありません。実際の設備・現場 PC に適用するときは、①対象の PC とネットワークについて設備保全部門・情報システム部門の承認を得る、②システム環境変数のようにPC 全体に効く変更は同じ PC で動く他部署のスクリプトを巻き添えにするため事前に影響範囲を確認する、③既存の CSV・ログを変換するときは必ず元ファイルを退避してから行う(errors="replace" で書き戻した文字は復元できません)、④24 時間稼働中の設備に繋がった PC では計画停止のタイミングで適用する——この 4 点が前提です。文字コード対策は、安全インターロック・安全 PLC・法定の警報装置の代替にはなりません。適用による設備の停止・データ欠損・機会損失について、筆者・GenbaPy は責任を負いません。