Pythonの組み込みファイル処理は、言語の真の強みの一つです — 基本的な読み書き操作にインポートは不要で、APIは午後一つで学べるほどシンプルです。しかし、チュートリアル版と実際に本番環境に出荷するものとの間には大きなギャップがあります。チュートリアル版はファイルを開き、読み込み、閉じます。本番版はデータをサイレントに破損させるエンコーディングの不一致、macOSでは動くがWindowsでクラッシュするパス、そして2GBのファイルで read() を呼び出すと静かにメモリを全て食い尽くすログファイルに対処します。この記事は、ハッピーパスだけでなく、実際に機能するパターンをカバーします。

with文 — 常に使う

Pythonのすべてのファイル処理例は コンテキストマネージャー — 読み込みの途中で例外が発生した場合でもファイルが確実に閉じられる with ブロックを使う必要があります。 コンテキストマネージャーは with ブロックへの入退出時の動作を定義するオブジェクトです; ファイルの場合、退出時に close() が自動的に呼び出されます。実際にそれが重要な理由を以下に示します:

python
# ❌ Manual close — works until it doesn't
f = open('app.log')
data = f.read()   # if this raises an exception...
f.close()         # ...this line never runs. File handle leaks.

# ✅ Context manager — close() is guaranteed
with open('app.log') as f:
    data = f.read()
# file is closed here, no matter what happened inside the block

長時間稼働するサーバーでは、これは机上の話ではありません — ファイルハンドルをリークし続けると最終的に OSError: [Errno 24] Too many open files が発生します。with 文は何のコストもかからず、 そのクラスのバグを完全に防ぎます。どこでも使いましょう。

ファイルの読み込み — 4つの方法、それぞれに適したツール

Pythonはファイルオブジェクトに複数のメソッドを提供しており、適切なものを選ぶことは ほとんどのチュートリアルが認めているよりも重要です:

  • f.read() — ファイル全体を一つの文字列として読み込む。小さな設定ファイルには問題ないが、大きなファイルには危険。
  • f.readline() — 内部ポインタを進めながら一度に一行ずつ読み込む。イテレーションを手動でコントロールしたい場合に便利。
  • f.readlines() — すべての行をリストに読み込む。便利だが、ファイル全体をメモリに読み込む。
  • for line in f: — イテレータープロトコル。ファイル全体を読み込まずに一行ずつ読む。デフォルトで使うべきもの。

現実的な例を示します: .env スタイルの設定ファイルを読み込んでディクショナリに変換する。これは実際に書くもので、人工的な「hello.txtを読む」デモではありません:

python
from pathlib import Path

def load_config(path: str) -> dict:
    """Read a key=value config file, ignoring comments and blank lines."""
    config = {}
    with open(path, encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            if '=' not in line:
                continue
            key, _, value = line.partition('=')
            config[key.strip()] = value.strip()
    return config

# Usage
settings = load_config('config/app.conf')
db_host = settings.get('DB_HOST', 'localhost')
.strip() の習慣: 行を読み込むとき、最後の行を除くすべての行に末尾の \n(Windowsでは \r\n)が含まれます。 line.strip() を呼び出して両方を削除してください。改行のみを取り除き、先頭の空白は保持したい場合は、代わりに line.rstrip('\n') を使用してください。

書き込みと追記 — どのモードがデータを破壊するかを知る

open() への2番目の引数はモードです。2つのモードが繰り返し問題を引き起こします:

  • 'w' — 書き込みモード。書き込み用にファイルを開く。ファイルが既に存在する場合、一文字も書く前に即座にゼロバイトに切り捨てられます。間違ったパスを開いた場合、サイレントなデータ破壊です。
  • 'a' — 追記モード。ファイルを開き、書き込みポインタを末尾に移動する。既存の内容は決して触れられない。新しい書き込みは既存のものの後に追加される。

追記モードの良い使用例はタイムスタンプ付きの構造化ログファイルの書き込みです。スクリプトや小さなサービスで役立つパターンを以下に示します:

python
import datetime

LOG_FILE = 'logs/pipeline.log'

def log_event(level: str, message: str) -> None:
    timestamp = datetime.datetime.utcnow().isoformat() + 'Z'
    line = f"[{timestamp}] {level.upper()}: {message}\n"
    with open(LOG_FILE, 'a', encoding='utf-8') as f:
        f.write(line)

log_event('info', 'Pipeline started')
log_event('warning', 'Retrying connection to database')
log_event('error', 'Failed to parse row 4821 — skipping')
警告: open(path, 'w') はファイルが存在しない場合に作成します — これは便利ですが、ファイルが存在する場合はサイレントに破壊します。タイプミスのパスがあると、エラーメッセージなしに本番ファイルを消去する可能性があります。ファイルを上書きすべきかどうか確信がない場合は、まず Path(path).exists() で確認するか、上書きの代わりに FileExistsError を発生させる 'x' モードを使用してください。

エンコーディング — 誰もが最終的に噛まれるバグ

これはPythonのファイル処理におけるサイレントなデータ破損の最も一般的な原因です。 エンコーディングを指定せずに open() を呼び出した場合のPython 3のデフォルトエンコーディングは locale.getpreferredencoding() によって決定されます — Windowsでは通常 cp1252、 Linux/macOSでは通常 UTF-8 です。つまり、Macで完璧に動作するコードが、ASCIIの範囲外の文字を含むファイルがある場合、Windowsサーバーでサイレントに文字化けしたりクラッシュしたりする可能性があります。 修正は引数を一つ追加するだけです:

python
# ❌ Platform-dependent — works on Linux, corrupts on Windows
with open('customers.csv') as f:
    data = f.read()

# ✅ Explicit UTF-8 — same behavior on every platform
with open('customers.csv', encoding='utf-8') as f:
    data = f.read()

# For files exported from Excel on Windows — may have a BOM (byte order mark)
# utf-8-sig strips the BOM automatically on read
with open('export.csv', encoding='utf-8-sig') as f:
    data = f.read()

BOM問題はMicrosoft ExcelからエクスポートされたCSVファイルで特に一般的です — ファイルは 隠し文字 \ufeff で始まり、間違ったエンコーディングで読むと  として現れるか、 最初の列ヘッダーが name の代わりに name のように見えます。 encoding='utf-8-sig' を使用すると透過的に処理されます。エンコーディング名の完全なリストについては Python codecsドキュメント を参照してください。

経験則: すべての open() 呼び出しに常に encoding='utf-8'(またはExcelエクスポートには 'utf-8-sig')を渡してください。習慣にしましょう — コストはゼロで、環境固有のバグのカテゴリ全体を排除できます。

パスの操作 — pathlibを使う

Pythonでファイルパスを構築する古い方法は文字列の連結または os.path.join() でした。現代的な方法は pathlib.Path で、 Python 3.4から利用可能で3.6から完全に成熟しています。考えなくてもWindowsとUnixでパスセパレーターを正しく処理し、 いくつかの os.path 呼び出しを読みやすい属性アクセスに置き換えます。

python
from pathlib import Path

# Build a path relative to the current script — works on Windows and Unix
base_dir = Path(__file__).parent
data_dir = base_dir / 'data'
input_file = data_dir / 'records.csv'

# Check existence before opening
if not input_file.exists():
    raise FileNotFoundError(f"Input file not found: {input_file}")

# Create a directory (including parents) without error if it already exists
output_dir = base_dir / 'output' / 'reports'
output_dir.mkdir(parents=True, exist_ok=True)

# Iterate over all JSON files in a directory
for json_file in data_dir.glob('*.json'):
    print(json_file.name)       # just the filename: 'records.json'
    print(json_file.stem)       # filename without extension: 'records'
    print(json_file.suffix)     # extension: '.json'
    print(json_file.parent)     # parent directory as a Path

# The / operator builds paths — no os.path.join needed
report_path = output_dir / f"report_{input_file.stem}.txt"

ここでの / 演算子は除算ではありません — Path はそれをパスの結合を意味するようにオーバーライドしています。これは自然に読め、文字列ベースのパス構築に伴うクォートとセパレーターの問題を排除します。もう一つ便利なメソッド: path.read_text(encoding='utf-8') は、ファイルの内容を文字列として取得したいだけの場合、open/read/closeパターンのショートカットです。

メモリを圧迫せずに大きなファイルを読む

ファイルが小さい場合 — 数メガバイト以下 — f.read() または f.readlines() で問題ありません。500MBのサーバーログや数ギガバイトのデータエクスポートの場合、 すべてをメモリに読み込むことは MemoryError またはOSによるプロセスキルへの近道です。 解決策は行単位のイテレーションです:

python
from pathlib import Path
from collections import Counter

def count_error_levels(log_path: str) -> dict:
    """
    Process a large log file line by line.
    Memory usage stays roughly constant regardless of file size.
    """
    counts = Counter()
    with open(log_path, encoding='utf-8') as f:
        for line in f:
            # Each line is fetched from disk as needed — not loaded all at once
            if ' ERROR ' in line:
                counts['error'] += 1
            elif ' WARN ' in line:
                counts['warning'] += 1
            elif ' INFO ' in line:
                counts['info'] += 1
    return dict(counts)

results = count_error_levels('/var/log/app/server.log')
print(f"Errors: {results.get('error', 0)}, Warnings: {results.get('warning', 0)}")

for line in f: パターンが機能する理由は、PythonのファイルオブジェクトがイテレータープロトコルをIするからです — 内部バッファを使用してディスクから一行ずつフェッチするため、ファイルサイズに関係なくメモリ使用量は本質的に一定です。一行ずつのイテレーションさえ十分に速くない本当に巨大なファイル(数十ギガバイト)には、 mmap を使ってファイルをメモリマップし、読み込むことなく正規表現で検索できます — しかしほとんどのユースケースでは、行イテレーターで十分です。

JSONとCSVの読み書き

Pythonの実際の作業で常に登場する2つのフォーマットがあり、両方ともクォート、エスケープ、構造を正しく処理する専用のstdlibモジュールを持っています — 文字列分割で解析しないでください。

python
import json
import csv
from pathlib import Path

# --- JSON ---
# Reading
with open('config/settings.json', encoding='utf-8') as f:
    settings = json.load(f)            # parsed directly from the file object

# Writing (indent=2 gives readable output)
with open('output/results.json', 'w', encoding='utf-8') as f:
    json.dump(results, f, indent=2, ensure_ascii=False)

# --- CSV ---
# Reading
with open('data/customers.csv', encoding='utf-8-sig', newline='') as f:
    reader = csv.DictReader(f)         # each row is a dict keyed by header
    for row in reader:
        process_customer(row['email'], row['plan'])

# Writing
fieldnames = ['id', 'email', 'plan', 'created_at']
with open('output/export.csv', 'w', encoding='utf-8', newline='') as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    for record in records:
        writer.writerow(record)

注目すべき点がいくつかあります: CSVファイルを開く際に newline='' を渡してください — csvモジュール は独自の行末を処理しており、Pythonのユニバーサル改行モードが干渉すると Windowsで重複した空白行が発生します。 JSON では、 ensure_ascii=False により非ASCII文字(アクセント付き文字、CJK文字など)が \uXXXX シーケンスにエスケープされるのではなく、そのまま書き込まれます — はるかに読みやすい出力になります。JSONやCSVデータを視覚的に検査または変換したい場合は、このサイトの JSONフォーマッターCSVフォーマッター がコードアプローチの良い補完となります。

エラーハンドリング — 見ることになる3つの例外

ファイル操作は予測可能な方法で失敗します。各ケースを明示的に処理することで、汎用的なトレースバックの代わりに実際に役立つエラーメッセージが得られます:

python
import json
from pathlib import Path

def load_json_config(path: str) -> dict:
    """
    Load a JSON config file with explicit error handling.
    Returns the parsed config or raises with a clear message.
    """
    config_path = Path(path)

    try:
        with open(config_path, encoding='utf-8') as f:
            return json.load(f)

    except FileNotFoundError:
        raise FileNotFoundError(
            f"Config file not found: {config_path.resolve()}\n"
            f"Create it or set CONFIG_PATH to the correct location."
        )

    except PermissionError:
        raise PermissionError(
            f"No read permission on {config_path.resolve()}\n"
            f"Check file ownership and mode (chmod 644 on Linux)."
        )

    except UnicodeDecodeError as e:
        raise ValueError(
            f"Encoding error reading {config_path}: {e}\n"
            f"Try opening with encoding='utf-8-sig' if the file came from Windows."
        )

    except json.JSONDecodeError as e:
        raise ValueError(
            f"Invalid JSON in {config_path} at line {e.lineno}, col {e.colno}: {e.msg}"
        )

4つの例外はほぼすべての実際の失敗モードをカバーしています: ファイルが存在しない、権限がない、エンコーディングが間違っている、またはコンテンツが不正。各メッセージは次の開発者(または午前2時の自分)に何が問題で、どこを見ればいいかを正確に伝えます。裸の Exception をキャッチして「何かがうまくいかなかった」と出力するのは有用なエラーハンドリングではありません — 混乱を下流に移動させるだけです。

再発生させるかNoneを返すかの判断: 設定ファイルの欠如がスクリプトの致命的なエラーである場合、呼び出し元が大きく失敗するように再発生させます(または新しい例外を発生させます)。オプション — たとえばユーザーごとの上書きファイル — の場合、FileNotFoundError をキャッチして None またはデフォルトのdictを返すことが適切です。関数ごとに一つの動作を選択して文書化してください。

まとめ

上記のすべての短いバージョン: 常に with ブロックを使い、常に encoding='utf-8' を渡し、パス構築に pathlib.Path を使い、サイズが不明な場合はファイル全体を読み込む代わりに行をイテレートする。これら4つの習慣は、本番環境に達する前にファイル処理バグの大多数を排除します。

さらに詳しくは: ファイルの読み書きに関するPythonチュートリアルセクション が基本を徹底的にカバーしています。 pathlibドキュメント はブックマークする価値があります — stdlibの最も有用な部分の一つで、ほとんどのPython開発者は活用し切れていません。 csvモジュールドキュメントjsonモジュールドキュメント は、それらのフォーマットを定期的に使用する場合に読む価値のあるエッジケース(カスタムデリミター、ストリーミングJSONなど)の良い例が含まれています。