バイナリモードとテキストモードの内部動作の違い

Python でファイルを開くとき、"r""rb" で何が違うのか。単に「改行コードの扱い」だけではない、内部動作の根本的な違いを解説する。

モードによる違いの概要

テキストモード("r", "w")

バイト列を文字列にデコード/エンコードする。改行コードの変換も行う

バイナリモード("rb", "wb")

バイト列をそのまま扱う。変換は一切行わない

テキストモードの内部処理

テキストモードでファイルを読むと、以下の処理が自動的に行われる。

ディスクからバイト列を読み込む

エンコーディングに従って文字列にデコード

改行コードを統一(\r\n → \n)

Python の str オブジェクトとして返す

# テキストモード
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()  # str 型
    print(type(content))  # <class 'str'>

バイナリモードの内部処理

バイナリモードでは変換が行われない。

ディスクからバイト列を読み込む

そのまま bytes オブジェクトとして返す

# バイナリモード
with open("data.txt", "rb") as f:
    content = f.read()  # bytes 型
    print(type(content))  # <class 'bytes'>

改行コードの変換

テキストモードでは OS に応じた改行コードの変換が行われる。

読み込み時

\r\n(Windows)や \r(古い Mac)を \n に統一する

書き込み時

\n を OS のネイティブ改行コードに変換する(Windows では \r\n)

# Windows で作成されたファイル(\r\n 改行)
# テキストモード
with open("windows.txt", "r") as f:
    content = f.read()
    print(repr(content))  # 'line1\nline2\n'(\r が消える)

# バイナリモード
with open("windows.txt", "rb") as f:
    content = f.read()
    print(repr(content))  # b'line1\r\nline2\r\n'(そのまま)

この変換を無効にするには newline="" を指定する。

# 改行変換を無効にする
with open("data.txt", "r", newline="") as f:
    content = f.read()  # \r\n がそのまま残る

エンコーディングの自動検出

テキストモードではエンコーディングを指定しないと、システムのデフォルトエンコーディングが使われる。

import locale

# デフォルトエンコーディングを確認
print(locale.getpreferredencoding())  # UTF-8 など

# 明示的に指定するのがベストプラクティス
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()
Linux / macOS

デフォルトは通常 UTF-8

Windows

デフォルトは CP932(Shift_JIS)などロケール依存。Python 3.15 から UTF-8 がデフォルトになる予定

バッファリングの違い

テキストモードとバイナリモードでバッファリングの動作も異なる。

# バイナリモードは buffering=0 が可能(バッファなし)
with open("data.bin", "rb", buffering=0) as f:
    pass  # OK

# テキストモードは buffering=0 不可
try:
    with open("data.txt", "r", buffering=0) as f:
        pass
except ValueError as e:
    print(e)  # can't have unbuffered text I/O

テキストモードでバッファなしにできないのは、エンコーディング変換がバッファを必要とするためだ。

io モジュールの構造

Python 3 の io モジュールは、階層的な構造を持っている。

FileIO

OS レベルの低レベル I/O。バイト列をそのまま扱う。

BufferedReader / BufferedWriter

FileIO をラップしてバッファリングを提供。

TextIOWrapper

BufferedReader/Writer をラップしてエンコーディング変換と改行処理を提供。

import io

# バイナリモードの内部構造
with open("data.txt", "rb") as f:
    print(type(f))  # <class '_io.BufferedReader'>
    print(type(f.raw))  # <class '_io.FileIO'>

# テキストモードの内部構造
with open("data.txt", "r") as f:
    print(type(f))  # <class '_io.TextIOWrapper'>
    print(type(f.buffer))  # <class '_io.BufferedReader'>
    print(type(f.buffer.raw))  # <class '_io.FileIO'>

seek() と tell() の違い

テキストモードでは seek()tell() の動作が複雑になる。

# バイナリモードはバイト位置で動作
with open("data.txt", "rb") as f:
    f.seek(10)  # 10 バイト目に移動
    pos = f.tell()  # 10

# テキストモードはエンコーディング依存
with open("data.txt", "r", encoding="utf-8") as f:
    # UTF-8 では 1 文字 = 1〜4 バイト
    # seek(10) が 10 文字目とは限らない
    f.seek(0)  # 先頭への seek は常に安全
    pos = f.tell()  # 不透明な値(実装依存)

テキストモードでは seek(0) 以外の seek() は避けるべきだ。任意の位置に移動したいならバイナリモードを使う。

使い分けの指針

用途モード理由
テキストファイルテキスト文字列として扱える、改行が統一される
画像・音声バイナリ変換されると壊れる
JSON・XMLテキスト文字列データとして解析する
ZIP・PDFバイナリバイナリフォーマット
ログ出力テキスト人間が読む文字列
ネットワークデータバイナリプロトコルに従う必要がある

まとめ

テキストモードの特徴

エンコーディング変換、改行変換、str 型で扱う。人間が読むファイル向け。

バイナリモードの特徴

変換なし、bytes 型で扱う。機械的なデータや正確なバイト操作が必要な場合向け。

迷ったら

エンコーディングを明示してテキストモードを使う。バイナリデータならバイナリモード。