日々のPythonプログラミングでは日本語を含むテキストを扱う場面が多いですが、表示や保存の段階で文字化けに悩む方は少なくありません。
この記事では「文字(str)」と「バイト(bytes)」の違い、そしてencode/decodeの使い方を、手を動かして理解できるように丁寧に解説します。
Pythonの文字(str)とバイト(bytes)の違い
文字(str)はUnicodeのテキスト
Python 3のstr
はUnicodeテキストです。
つまり、内部的には「あ」や「é」などの文字そのものを表し、特定の保存形式(UTF-8などのバイト列)とは切り離されて管理されています。
画面に表示したり、文字数を数えたりする操作はstr
で行います。
バイト(bytes)は生のバイト列(バイナリ)
一方、bytes
は0〜255の数値(バイト)が並んだ配列です。
画像や圧縮ファイルなどのバイナリデータはもちろん、テキストも保存や送受信の段階では最終的にbytes
になります。
文字とバイトは別物であり、必要に応じて相互変換(エンコード/デコード)します。
型とリテラルの書き方(b’…’, bytearray)
bytes
はb'...'
というリテラルで書けます。
変更可能な類似型bytearray
もあります。
# str は Unicode テキスト
s = "こんにちは" # 日本語の文字列
# bytes はバイト列。b'' リテラルで書ける
b = b"ABC" # ASCII 範囲のバイトはそのまま書ける
# エスケープで任意のバイトを表現
b_utf8_fragment = b"\xe3\x81\x82" # UTF-8 で「あ」に相当するバイト断片
# bytearray は変更可能なバイト配列
ba = bytearray(b"ABC")
ba[0] = 0x61 # 'A'(0x41) を 'a'(0x61) に変更
print(type(s), type(b), type(ba))
print(ba)
<class 'str'> <class 'bytes'> <class 'bytearray'>
bytearray(b'aBC')
len()の違い(文字数とバイト数)
len()
はstr
では文字の数、bytes
ではバイト数を返します。
文字とバイトで結果が異なる点に注意します。
s = "あ" # 1 文字
print(len(s)) # 文字数
b_utf8 = s.encode("utf-8") # UTF-8 では「あ」は 3 バイト
b_cp932 = s.encode("cp932") # CP932(Shift_JIS) では 2 バイト
print(len(b_utf8)) # バイト数
print(len(b_cp932)) # バイト数
1
3
2
絵文字や結合文字では、見た目1文字でもUnicode的には複数コードポイントで構成される場合があり、len(str)
が2以上になることがあります。
エンコーディングとデコードの基礎(encode/decode)
文字→バイトに変換する(encode, UTF-8)
エンコード(encoding)はstr
をbytes
に変換する操作です。
もっとも一般的なのはUTF-8です。
text = "Pythonで文字化けを防ぐ"
data = text.encode("utf-8") # 文字 → バイト
print(type(data), data[:10]) # 先頭10バイトだけ確認
<class 'bytes'> b'Python\xe3\x81'
バイト→文字に変換する(decode, UTF-8)
デコード(decoding)はbytes
をstr
に戻す操作です。
エンコードに使ったのと同じ文字コードを指定します。
data = b'Python\xe3\x81\xa7\xe6\x96\x87\xe5\xad\x97'
text = data.decode("utf-8") # バイト → 文字
print(type(text), text)
<class 'str'> Pythonで文字
エンコードとデコードで文字コードが一致しないとエラーや文字化けが発生します。
よく使うエンコーディング(UTF-8, CP932, Shift_JIS)
以下は主な文字コードの概要です。
エンコーディング | 概要 | 主な用途/備考 |
---|---|---|
UTF-8 | 世界中の文字を扱えるUnicodeの可変長方式 | 現在の標準。Web/クロスプラットフォームに最適 |
CP932 | Windows日本語の実装。いわゆるShift_JIS互換 | Windowsコンソール/レガシーファイルに登場 |
Shift_JIS | 旧来の日本語文字コード | 新規採用は非推奨。レガシーデータで遭遇 |
新規作成は基本UTF-8、過去データや外部仕様でCP932/Shift_JISを扱うことがあります。
エラー処理(errors=strict/ignore/replace/backslashreplace)
変換時に表せない文字に遭遇すると、デフォルトのerrors="strict"
では例外が出ます。
他のモードで挙動を制御できます。
# CP932 に存在しない文字の例: "𝄞"(U+1D11E, 楽譜のト音記号)
s = "音符𝄞"
# strict: 例外
try:
s.encode("cp932", errors="strict")
except UnicodeEncodeError as e:
print("strict:", e)
# ignore: 失われる(消える)
print("ignore:", s.encode("cp932", errors="ignore"))
# replace: 置換(通常 '?')
print("replace:", s.encode("cp932", errors="replace"))
# backslashreplace: \uXXXX などのエスケープに
print("backslashreplace:", s.encode("cp932", errors="backslashreplace"))
strict: 'cp932' codec can't encode character '\U0001d11e' in position 2: illegal multibyte sequence
ignore: b'\x89\xb9\x95\x84'
replace: b'\x89\xb9\x95\x84?'
backslashreplace: b'\x89\xb9\x95\x84\\U0001d11e'
文字化けの原因(エンコーディング不一致)
エンコードに使った文字コードと、デコードに使う文字コードが一致しない場合に文字化けや例外が起きます。
s = "日本語"
b_cp932 = s.encode("cp932") # CP932 でエンコード
# 間違って別の文字コードでデコードすると…
try:
print(b_cp932.decode("utf-8")) # 多くの場合はエラー
except UnicodeDecodeError as e:
print("UTF-8 ではデコードできません:", e)
# 無理やり latin-1 でデコードすると、エラーは出ないが文字化けした表示になる
print("latin-1 で無理やり読むと:", b_cp932.decode("latin-1"))
UTF-8 ではデコードできません: 'utf-8' codec can't decode byte 0x93 in position 0: invalid start byte
latin-1 で無理やり読むと: “î—€
「エラーが出ない=正しい」ではありません。
意味のあるテキストに戻すには正しいエンコーディングの把握が不可欠です。
実践での使い分け(ファイル/ネットワーク)
ファイル読み書きはencodingを指定(open, encoding)
テキストファイルは必ずencoding=...
を指定します。
新規はUTF-8が無難です。
# UTF-8 でファイルに書く
text = "こんにちは、世界"
with open("hello.txt", "w", encoding="utf-8") as f:
f.write(text)
# UTF-8 で読み戻す
with open("hello.txt", "r", encoding="utf-8") as f:
loaded = f.read()
print(loaded)
こんにちは、世界
外部仕様がCP932ならencoding="cp932"
を使います。
テキストモードとバイナリモード(r/wとrb/wb)
open()
にはテキストモード("r"
/"w"
)とバイナリモード("rb"
/"wb"
)があります。
- テキストモード:
str
で読み書き。encoding
が使われる。 - バイナリモード:
bytes
で読み書き。エンコーディングは関与しない。
# バイナリモードで任意バイトを書き込み
data = bytes([0x41, 0x42, 0x43]) # b"ABC"
with open("out.bin", "wb") as f:
f.write(data)
# 読み戻すと bytes が得られる
with open("out.bin", "rb") as f:
raw = f.read()
print(raw, type(raw))
b'ABC' <class 'bytes'>
ソケット通信はbytesを送受信(send/recv)
ソケットはバイト列でやり取りします。
テキストプロトコルなら送信前にencode()
、受信後にdecode()
します。
import socket
# HTTP でヘッダは ASCII 範囲のみ想定。安全のため ASCII でエンコード
with socket.create_connection(("example.com", 80), timeout=5) as sock:
request = "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n"
sock.sendall(request.encode("ascii")) # str → bytes
chunk = sock.recv(100) # bytes が返る
print(type(chunk), len(chunk))
<class 'bytes'> 100
標準入出力のエンコーディングと環境依存
標準入出力の文字コードは環境依存です。
対話環境やOSにより異なるため、確認しておくと安全です。
import sys, locale
print("stdout encoding:", sys.stdout.encoding)
print("preferred encoding:", locale.getpreferredencoding(False))
stdout encoding: utf-8
preferred encoding: cp932
Windowsの古いコンソールではcp932
になる場合があります。
必要に応じてPYTHONIOENCODING=utf-8
を設定したり、端末のコードページをUTF-8に変更します。
よくあるエラーと対処法
strとbytesを混ぜてTypeErrorになる
strとbytesは直接結合できません。
必ずどちらかに揃えます。
try:
result = b"Hello, " + "世界" # 型が違う
except TypeError as e:
print("TypeError:", e)
# 正しい方法: str 同士で連結し、最後に encode する
text = "Hello, " + "世界"
data = text.encode("utf-8")
print(type(data), data)
TypeError: can't concat str to bytes
<class 'bytes'> b'Hello, \xe4\xb8\x96\xe7\x95\x8c'
ダブルエンコード/デコードを避ける
Python 3では、str
に.decode()
はありません。
bytes
に.encode()
も基本的に不要です。
「文字にencode」「バイトにdecode」だけが正しい対応です。
s = "あ"
b = s.encode("utf-8") # OK: str → bytes
# 間違い1: str に decode は存在しない
try:
s.decode("utf-8")
except AttributeError as e:
print("誤用(1):", e)
# 間違い2: bytes に encode は不要(多くの場面で誤用の合図)
try:
b2 = b.encode("utf-8") # Python の実装では AttributeError になることがあります
except AttributeError as e:
print("誤用(2):", e)
# 正しい往復
s2 = b.decode("utf-8") # OK: bytes → str
print(s2)
誤用(1): 'str' object has no attribute 'decode'
誤用(2): 'bytes' object has no attribute 'encode'
あ
加えて、すでにUTF-8のbytes
を得ているのに再度utf-8
でエンコードしようとすると誤りです。
バイト列はそのまま扱うか、文字として処理したい場合のみdecode()
します。
BOM付きUTF-8を扱う(utf-8-sig)
一部のツールやExcel向けにBOM(Byte Order Mark)付きUTF-8が必要な場合があります。
Pythonではutf-8-sig
を使うとBOMを自動付与/除去できます。
text = "先頭にBOMを付けたい"
# 書き込み時に BOM を付与
with open("bom.txt", "w", encoding="utf-8-sig") as f:
f.write(text)
# 読み込み時、utf-8 だと先頭に不可視文字(\ufeff)が残る場合がある
with open("bom.txt", "r", encoding="utf-8") as f:
raw = f.read()
print("utf-8 で読むと:", repr(raw)) # '\ufeff' が見える
# utf-8-sig なら BOM を除去してくれる
with open("bom.txt", "r", encoding="utf-8-sig") as f:
clean = f.read()
print("utf-8-sig で読むと:", repr(clean))
utf-8 で読むと: '\ufeff先頭にBOMを付けたい'
utf-8-sig で読むと: '先頭にBOMを付けたい'
Windowsのファイルパスとエンコーディング注意
WindowsではファイルシステムAPIはUnicode対応なので、str
のパス(日本語名を含む)はそのまま扱えます。
一方で、コンソール入出力は環境によりcp932
などになるため、表示が文字化けすることがあります。
その場合は以下を検討します。
- 端末のコードページをUTF-8に変更する(
chcp 65001
)、もしくはWindows Terminal/VS Codeターミナルを使う。 - 環境変数
PYTHONIOENCODING=utf-8
を設定する。 - ファイルの読み書きでは
encoding="utf-8"
を明示する。 - パス操作は
pathlib.Path
を使い、str
で一貫して扱う。
from pathlib import Path
p = Path("日本語ファイル.txt")
p.write_text("内容です", encoding="utf-8")
print(p.read_text(encoding="utf-8"))
内容です
まとめ
Pythonのstr
はUnicodeの文字、bytes
は生のバイト列であり、両者は明確に別物です。
テキストを保存・送受信する際にはエンコード(文字→バイト)とデコード(バイト→文字)を正しく対応付け、encoding
を明示することで文字化けを防げます。
特にUTF-8を基本とし、レガシー環境ではCP932/Shift_JISに合わせる、エラー時にはerrors
パラメータを活用する、といった実践的な方針が有効です。
ファイルはopen(..., encoding=...)
、ネットワークはbytes
中心、標準入出力は環境依存という前提を押さえ、「文字とバイトを混同しない」ことを常に意識してコーディングしてください。