Pythonのファイル読み込み・書き込みとは、with open(パス, モード, encoding="utf-8") as f: でファイルを開き、f.read() や f.write() で中身をやり取りする操作です。 覚える要素は「モード(r・w・a・x)」「with」「encoding」の3つで、この3つを毎回書くだけで、閉じ忘れと文字化けの大半を避けられます。

この記事では、モードごとに既存ファイルがどうなるか、read・readline・readlines・for の違い、writelines に改行が付かないこと、with を使わないと書き込みが反映されない様子、encoding を省いたときの既定値、UnicodeDecodeError と文字化けの再現と直し方、BOM、newline、よく出る例外までを扱います。コードはすべて Python 3.14.7(Linux)で実行し、出力をそのまま載せています。 Windows 固有の動作など手元で動かせなかった部分は「公式ドキュメントで確認」と明記しました。

Pythonファイル読み込み・書き込みの早見表

まずはコピペ用の形です。迷ったらこの表の書き方をそのまま使えば用が足ります。

やりたいこと 書き方 ポイント
全体を文字列で読む with open(p, encoding="utf-8") as f: s = f.read() モード省略時は "r"(読み込み・テキスト)
1行ずつ読む for line in f: 行末の改行は line.rstrip("\n") で外す
行のリストで読む f.read().splitlines() 改行なしのリストになる
新規作成・上書き open(p, "w", encoding="utf-8") 既存の中身は開いた時点で空になる
末尾に追記 open(p, "a", encoding="utf-8") なければ新規作成
あれば止める(新規専用) open(p, "x", encoding="utf-8") 既存なら FileExistsError
複数行を書く f.writelines(s + "\n" for s in lines) writelines は改行を足さない
Excel 向け CSV の文字コード encoding="utf-8-sig" BOM 付き UTF-8
Shift_JIS のファイル encoding="cp932" Windows の日本語ファイルはこちらが合う
画像などのバイナリ open(p, "rb") / open(p, "wb") encoding は付けない
1行で読み書き Path(p).read_text(encoding="utf-8") pathlib。開閉を任せられる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Pythonファイル読み込み・書き込み|open・with・encoding一覧」

まずはコピペで動く基本形

前提は Python 3.8 以降で、追加のインストールは不要です。次のコードを basic.py として保存し、python basic.py で実行します。

# basic.py
# 1) 書き込み(なければ作成、あれば上書き)
with open("memo.txt", "w", encoding="utf-8") as f:
    f.write("りんご\n")
    f.write("みかん\n")

# 2) 追記
with open("memo.txt", "a", encoding="utf-8") as f:
    f.write("ぶどう\n")

# 3) 読み込み(全体)
with open("memo.txt", encoding="utf-8") as f:
    text = f.read()
print(repr(text))

# 4) 読み込み(1行ずつ)
with open("memo.txt", encoding="utf-8") as f:
    for i, line in enumerate(f, 1):
        print(i, line.rstrip("\n"))

実行結果(Python 3.14.7):

'りんご\nみかん\nぶどう\n'
1 りんご
2 みかん
3 ぶどう

open() の引数は「パス、モード、encoding」の順に3つ書く、と形で覚えるのがおすすめです。with ブロックを抜けると、ファイルは自動で閉じられます。

open()のモード一覧と、既存ファイルへの影響

モードは、「何をするか(r・w・a・x)」と「中身の種類(t・b)」の組み合わせです。+ を付けると読み書き両用になります。

基本4モードと + の一覧

中身が old の1行だけ入った m.txt に対し、各モードで開いて NEW を書いたときの結果です(Python 3.14.7 で確認)。

モード 意味 既存ファイルがあるとき ファイルがないとき 読み 書き
r(既定) 読み込み そのまま読める FileNotFoundError できる できない
w 書き込み 開いた時点で空になる → NEW 新規作成 できない できる
a 追記 末尾に足す → old+NEW 新規作成 できない できる
x 新規専用 FileExistsError 新規作成 できない できる
r+ 読み書き 先頭から上書き(残りは残る) FileNotFoundError できる できる
w+ 読み書き 空にしてから読み書き 新規作成 できる できる
a+ 読み+追記 書き込みは常に末尾 新規作成 できる できる

実際の出力は次のとおりです。

r exist -> 'old\n'
r none  -> FileNotFoundError [Errno 2] No such file or directory: 'm.txt'
w exist -> 'NEW\n'
a exist -> 'old\nNEW\n'
x exist -> FileExistsError [Errno 17] File exists: 'm.txt'
x none  -> 'NEW\n'
r+ exist -> 'NEW\n'
a+ exist -> 'old\nNEW\n'

w は「書いた瞬間」ではなく「開いた瞬間」に中身が空になります。 既存の内容を残したいときは a、上書きを避けたいときは x を選びます。

r+ は「先頭から同じ文字数だけ置き換える」動きです。abcdef の入ったファイルに XY を書くと、次のようになりました。

with open("m.txt", "r+", encoding="utf-8") as f:
    f.write("XY")
# ファイルの中身: 'XYcdef\n'

また a+ で開いた直後は読み書き位置が末尾にあるため、f.read() は空文字列を返します。f.seek(0) で先頭に戻すと読めます。

''            ← a+ で開いてすぐ read()
'XYcdef\n'    ← seek(0) のあと read()

t(テキスト)と b(バイナリ)

指定 読み書きする型 encoding 改行の変換 向いているもの
t(既定。"r" は "rt" と同じ) str 使う する テキスト、CSV、JSON、ログ
b("rb" "wb" "ab" "xb") bytes 使わない しない 画像、PDF、ZIP、文字コード不明のファイル
with open("utf8b.txt", "rb") as f:
    b = f.read()
print(type(b), b[:6], b.decode("utf-8")[:3])
<class 'bytes'> b'\xe3\x81\x93\xe3\x82\x93' こんに

バイナリモードでは str をそのまま書けません。encode() で bytes にしてから書きます。

TypeError a bytes-like object is required, not 'str'      ← f.write("文字列")
9                                                          ← f.write("文字列".encode("utf-8")) の戻り値(バイト数)

モードを選ぶ判断表

状況 選ぶモード
読むだけ r(省略可)
毎回まっさらな内容で作り直す w
ログのように足していく a
既存のファイルを上書きしたくない x
全体を読んで加工し、書き戻す r で読む → w で書く(2回開く)
画像・PDF・圧縮ファイル rb / wb

「読んで書き戻す」は r+ 1回より、r と w の2回に分けるほうが読みやすく、位置の管理も要りません。

ファイルを読み込む(read・readline・readlines・for)

読み込み方は4通りあります。同じ memo.txt(3行)で結果を比べました。

with open("memo.txt", encoding="utf-8") as f:
    print("read     ", repr(f.read()))
with open("memo.txt", encoding="utf-8") as f:
    print("read(4)  ", repr(f.read(4)), repr(f.read(4)))
with open("memo.txt", encoding="utf-8") as f:
    print("readline ", repr(f.readline()), repr(f.readline()))
with open("memo.txt", encoding="utf-8") as f:
    print("readlines", f.readlines())
with open("memo.txt", encoding="utf-8") as f:
    for i, line in enumerate(f, 1):
        print(i, repr(line), repr(line.rstrip("\n")))
with open("memo.txt", encoding="utf-8") as f:
    print("splitlines", f.read().splitlines())
read      'りんご\nみかん\nぶどう\n'
read(4)   'りんご\n' 'みかん\n'
readline  'りんご\n' 'みかん\n'
readlines ['りんご\n', 'みかん\n', 'ぶどう\n']
1 'りんご\n' 'りんご'
2 'みかん\n' 'みかん'
3 'ぶどう\n' 'ぶどう'
splitlines ['りんご', 'みかん', 'ぶどう']

4つの読み方の比較表

書き方 戻り値 行末の改行 メモリ 向いている場面
f.read() 全体を1つの str 含む 全体ぶん使う 小さなファイルをまるごと扱う
f.read(n) 先頭から n 文字(バイナリは n バイト) 含む n ぶん 固定サイズで区切って読む
f.readline() 1行ぶんの str 含む 1行ぶん 先頭行(見出し)だけ先に読む
f.readlines() 行の list 各要素に含む 全体ぶん使う 行番号で何度も参照する
for line in f: 1行ずつ 含む 1行ぶん 行ごとの処理全般(基本はこれ)
f.read().splitlines() 行の list 含まない 全体ぶん使う 改行なしのリストがほしい

行ごとに処理するなら for line in f: が基本です。 ファイルオブジェクトそのものが「1行ずつ返す反復可能オブジェクト」なので、readlines() を挟まずにそのまま回せます。

改行を外す:rstrip と splitlines

readline()・readlines()・for で得た行には、行末の \n が残っています。 そのまま print() すると空行が1つ余分に出るのはこのためです。

方法 結果 注意点
line.rstrip("\n") 改行だけ外す 行末の空白は残る
line.rstrip() 行末の空白・タブ・改行を外す 意味のある行末空白も消える
line.strip() 行頭と行末の空白を外す インデントも消える
f.read().splitlines() 改行なしのリスト 全体を読み込む

2回目の read() が空になる理由

ファイルオブジェクトは「いまどこまで読んだか」という位置を持っています。最後まで読んだあとにもう一度 read() すると、空文字列が返ります。

with open("memo.txt", encoding="utf-8") as f:
    a = f.read()
    b = f.read()
    print("twice", len(a), repr(b))
    f.seek(0)
    print("after seek", repr(f.readline()))
twice 12 ''
after seek 'りんご\n'

もう一度読みたいときは f.seek(0) で先頭に戻すか、最初の read() の結果を変数に持っておきます。

ファイルに書き込む(write・writelines・print)

with open("out.txt", "w", encoding="utf-8") as f:
    n = f.write("1行目")
    print("write returns", n)
    f.write("2行目\n")
    f.writelines(["a", "b", "c"])
    f.writelines(s + "\n" for s in ["x", "y"])
    print("p", 1, 2, sep=",", file=f)

with open("out.txt", encoding="utf-8") as f:
    print(repr(f.read()))
write returns 3
'1行目2行目\nabcx\ny\np,1,2\n'

出力から読み取れることは3つです。

  • write() は改行を付けません。 "1行目" と "2行目" がつながっています。改行したい位置に自分で \n を書きます。
  • writelines() も改行を付けません。 名前に lines とありますが、リストの要素を順に書くだけなので abc と続きます。s + "\n" のように足してから渡します。
  • print(..., file=f) は末尾に改行を付けます。 数値もそのまま渡せ、sep や end も使えます。

書き込み方法の比較表

書き方 渡せるもの 改行 戻り値
f.write(s) str 1つ 付かない 書いた文字数
f.writelines(items) str の反復可能オブジェクト 付かない None
print(a, b, file=f) 何でも(自動で文字列化) 末尾に付く None
f.write("\n".join(lines) + "\n") str のリスト 行間と末尾に付く 書いた文字数

数値を write すると TypeError

write() に渡せるのは文字列だけです。

TypeError write() argument must be str, not int

f.write(str(123)) や f.write(f"{count}\n") のように、文字列にしてから渡します。

モードと操作が合わないとき

読み込み用に開いたファイルへ書く、書き込み用に開いたファイルを読む、という組み合わせは io.UnsupportedOperation になります。

UnsupportedOperation not writable     ← "r" で開いて write()
UnsupportedOperation not readable     ← "w" で開いて read()

モードを見直すと解決します。

withを使う理由:閉じ忘れると書き込まれない

write() は、すぐにはディスクへ書きません。いったんメモリ上のバッファにため、閉じるとき(または flush() のとき)にまとめて書き出します。 with を使わずに確かめます。

import os

f = open("noclose.txt", "w", encoding="utf-8")
f.write("hello\n")
print("before close size", os.path.getsize("noclose.txt"))
f.flush()
print("after flush size", os.path.getsize("noclose.txt"))
f.close()
print("closed", f.closed)
before close size 0
after flush size 6
closed True

write() した直後のファイルサイズは 0 バイト でした。この状態で別のプログラムがファイルを読んでも、中身は空に見えます。close() を書き忘れたり、途中で例外が起きて close() の行まで届かなかったりすると、「書いたはずの内容が入っていない」という結果になります。

with は例外が起きても閉じてくれる

try:
    with open("w.txt", "w", encoding="utf-8") as f:
        f.write("before\n")
        1 / 0
except ZeroDivisionError:
    print("exc; closed=", f.closed)

with open("w.txt", encoding="utf-8") as f:
    print(repr(f.read()))
exc; closed= True
'before\n'

ブロックの途中で例外が起きても、with はファイルを閉じ、そこまでに書いた内容をディスクへ書き出します。

直す前と直した後

コード 例外時
直す前 f = open(p, "w") → f.write(s) → f.close() close() に届かず、内容が残らないことがある
try/finally try: f.write(s) / finally: f.close() 閉じられるが、行数が増える
直した後 with open(p, "w", encoding="utf-8") as f: f.write(s) 自動で閉じる

閉じたあとのファイルを操作すると、次のエラーになります。with ブロックの外で f を使っていないかを確認します。

ValueError I/O operation on closed file.

2つのファイルを同時に開く

読み込み元と書き込み先を1つの with に並べられます。Python 3.10 以降は、丸かっこで囲んで複数行に分けることもできます。

with (
    open("memo.txt", encoding="utf-8") as src,
    open("copy.txt", "w", encoding="utf-8") as dst,
):
    for line in src:
        dst.write(line)

encodingは必ず書く:未指定の既定値とPEP 686

encoding は、ファイルのバイト列と Python の文字列を対応づける規則(文字コード) の指定です。省略すると、Python は実行環境に合わせた既定値を使います。

既定値は環境で変わる

環境 encoding 未指定のときの既定値 確認方法
Python 3.14 まで・Windows(日本語) cp932(Shift_JIS 系) 公式ドキュメントで確認
Python 3.14 まで・macOS / 一般的な Linux UTF-8(ロケールが UTF-8 のため) 公式ドキュメントで確認
Python 3.14.7・Linux・ロケール C(今回の検証環境) UTF-8(UTF-8 モードが自動で有効) 実行して確認
同じ環境で PYTHONUTF8=0 LC_ALL=C を指定 ANSI_X3.4-1968(ASCII) 実行して確認
Python 3.15 以降 どの OS でも UTF-8(PEP 686) 公式ドキュメントで確認

手元で確かめた結果です。

# enc.py
import sys, locale
with open("memo.txt") as f:
    print(sys.flags.utf8_mode, locale.getencoding(), f.encoding)
$ python3.14 enc.py
1 UTF-8 utf-8
$ PYTHONUTF8=0 LC_ALL=C python3.14 enc.py
0 ANSI_X3.4-1968 ANSI_X3.4-1968
$ PYTHONUTF8=0 LC_ALL=C python3.14 -c "print(open('memo.txt').read())"
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 0: ordinal not in range(128)

同じコード・同じファイルでも、環境変数が違うだけで読めたり読めなかったりします。 「自分のパソコンでは動くのに、別の環境ではエラーになる」という現象の多くは、この既定値の違いから来ています。encoding="utf-8" を書いておけば、どの環境でも同じ結果になります。

Python 3.15 で UTF-8 が既定に(PEP 686)

PEP 686「Make UTF-8 mode default」は Final(採択・実装済み)で、対象は Python 3.15 です。公式の PEP に書かれている要点は次のとおりです(3.15 は手元で動かしていないため、公式ドキュメントで確認した内容です)。

項目 内容
変わること UTF-8 モードが既定で有効になり、ファイル・標準入出力・パイプの既定が UTF-8 にそろう
影響が出やすい環境 主に Windows(多くの Unix 系はすでに UTF-8)
以前の動作に戻す 環境変数 PYTHONUTF8=0、または -X utf8=0
ロケールの文字コードを明示して使う open(p, encoding="locale")
ロケールの文字コードを調べる locale.getencoding()(Python 3.11 以降)

3.15 以降も、Shift_JIS のファイルを読むには encoding="cp932" の指定が必要です。 既定値が変わっても、「ファイルの文字コードに合わせて書く」という基本は同じです。

書き忘れを見つける:-X warn_default_encoding

encoding を省いた箇所を知らせてくれるオプションが、Python 3.10 から使えます。

# enc2.py
from pathlib import Path
with open("memo.txt") as f:
    f.read()
Path("memo.txt").read_text()
with open("memo.txt", encoding="utf-8") as f:
    f.read()
with open("memo.txt", encoding="locale") as f:
    print(f.encoding)
$ python3.14 -X warn_default_encoding enc2.py
enc2.py:2: EncodingWarning: 'encoding' argument not specified
  with open("memo.txt") as f:
enc2.py:4: EncodingWarning: 'encoding' argument not specified
  Path("memo.txt").read_text()
UTF-8

(パスは短く表記しています。)encoding を書いた行と encoding="locale" の行には警告が出ていません。環境変数 PYTHONWARNDEFAULTENCODING=1 でも同じ効果があり、-W error::EncodingWarning を足すと警告を例外に変えられるので、テスト実行時に付けておくと書き忘れをまとめて洗い出せます。

UnicodeDecodeErrorと文字化けを再現して直す

文字コードが合わないときの現れ方は、「エラーで止まる」と「エラーにならず文字化けする」の2通りあります。

再現1:Shift_JIS のファイルを UTF-8 で読む → エラー

from pathlib import Path
Path("sjis.txt").write_text("日本語のメモ\n", encoding="cp932")

with open("sjis.txt", encoding="utf-8") as f:
    f.read()
UnicodeDecodeError 'utf-8' codec can't decode byte 0x93 in position 0: invalid start byte

再現2:UTF-8 のファイルを cp932 で読む → 文字化け、またはエラー

Path("utf8b.txt").write_text("こんにちは\n", encoding="utf-8")
with open("utf8b.txt", encoding="cp932") as f:
    print(repr(f.read()))
'縺薙s縺ォ縺。縺ッ\n'

エラーにならず、別の文字として読めてしまいました。 「縺」「繧」「ォ」が並ぶ文字化けは、UTF-8 のファイルを Shift_JIS 系で読んだときの典型です。なお内容によってはエラーになります(日本語のメモ を同じように読むと 'cp932' codec can't decode byte 0x81 in position 10: illegal multibyte sequence でした)。

直し方:ファイルに合う encoding を指定する

with open("sjis.txt", encoding="cp932") as f:
    print(repr(f.read()))
'日本語のメモ\n'

エラーメッセージから文字コードを推測する表

症状 考えられる状況 試す指定
'utf-8' codec can't decode byte 0x93(0x82〜0x9f、0xe0 台など) Shift_JIS 系のファイル encoding="cp932"
'cp932' codec can't decode byte ... UTF-8 のファイルを Windows の既定で読んだ encoding="utf-8"
'ascii' codec can't decode byte 0xe3 ロケールが C / POSIX encoding="utf-8"
縺 繧 ォ が並ぶ UTF-8 を cp932 で読んだ encoding="utf-8"
ã に続いて記号が並ぶ UTF-8 を latin-1 で読んだ encoding="utf-8"
先頭に見えない1文字が付く BOM 付き UTF-8 encoding="utf-8-sig"
'utf-8' codec can't decode byte 0xff in position 0 UTF-16(BOM が FF FE) encoding="utf-16"

shift_jis と cp932 の違い

名前は似ていますが、扱える文字の範囲が違います。丸数字の「①」を書き込んで確かめました。

指定 「①」の書き込み 結果
encoding="shift_jis" できない UnicodeEncodeError(illegal multibyte sequence)
encoding="cp932" できる 丸数字や「髙」などの拡張文字も書ける

Windows で作られた日本語ファイルには cp932 を指定するのが合います。

errors 引数:読めないバイトの扱いを決める

文字コードがどうしても合わない部分があるときは、errors で扱いを選べます。Shift_JIS のファイルを、あえて UTF-8 で読んだ結果です。

errors 動作 上の sjis.txt を読んだ結果
"strict"(既定) 例外を出す UnicodeDecodeError
"replace" 読めない部分を � に置き換える '���{��̃���\n'
"ignore" 読めない部分を捨てる '{̃\n'
"backslashreplace" \x93 の形で残す \x93\xfa\x96{… の形

errors は「間違った文字コードを正しく読む」機能ではなく、「止まらずに進む」ための指定です。 ignore は情報が黙って消えるので、まずは正しい encoding を探し、どうしても壊れたバイトが混ざるログなどに限って replace や backslashreplace を使う、という順番がおすすめです。

書き込み側のエラー:UnicodeEncodeError

cp932 に存在しない文字(絵文字など)を書こうとすると、書き込み側でエラーになります。

with open("e.txt", "w", encoding="cp932") as f:
    f.write("絵文字😀")
UnicodeEncodeError(encoding=cp932、reason=illegal multibyte sequence、position 3)

errors を指定したときの結果は次のとおりです。

errors ファイルに書かれる内容
"replace" 絵文字?
"xmlcharrefreplace" 絵文字&#128512;
"ignore" 絵文字

相手の指定がなければ、書き込みは UTF-8 にしておくと、絵文字を含むすべての文字を扱えます。

文字コードがわからないファイルを読む関数

UTF-8(BOM あり・なし)と cp932 の順に試す、コピペ用の関数です。

def read_text_safely(path, encodings=("utf-8-sig", "cp932")):
    """候補の文字コードを順に試し、(本文, 使った文字コード) を返す"""
    for enc in encodings:
        try:
            with open(path, encoding=enc) as f:
                return f.read(), enc
        except UnicodeDecodeError:
            continue
    raise ValueError(f"{path}: 文字コードを判定できませんでした")

print(read_text_safely("sjis.txt"))
print(read_text_safely("utf8.txt"))
print(read_text_safely("bom.txt"))
('日本語のメモ\n', 'cp932')
('日本語のメモ\n', 'utf-8-sig')
('id,name\n', 'utf-8-sig')

候補は「厳しい順」に並べます。 UTF-8 は規則が厳密で、別の文字コードのファイルを読むとほぼエラーになるため先頭に置きます。cp932 は多くのバイト列を受け入れてしまうので後ろに置きます。

BOM付きUTF-8は utf-8-sig で扱う

BOM(Byte Order Mark)は、ファイルの先頭に置かれる目印のバイト列(UTF-8 では EF BB BF)です。Windows のメモ帳や Excel が保存した UTF-8 のファイルに付いていることがあります。

from pathlib import Path
Path("bom.txt").write_text("id,name\n", encoding="utf-8-sig")
print(Path("bom.txt").read_bytes())

s = open("bom.txt", encoding="utf-8").read()
print(len(s), s.startswith("id"), hex(ord(s[0])))

s = open("bom.txt", encoding="utf-8-sig").read()
print(len(s), s.startswith("id"))
b'\xef\xbb\xbfid,name\n'
9 False 0xfeff
8 True

encoding="utf-8" で読むと、先頭に目に見えない1文字(U+FEFF)が残り、文字数が 9 になりました。 startswith("id") が False になっているとおり、「CSV の最初の列名だけ一致しない」「辞書のキーが見つからない」という現象の原因になります。

指定 読み込み 書き込み
utf-8 BOM があると先頭に1文字残る BOM を付けない
utf-8-sig BOM があれば外し、なければそのまま読む BOM を付ける

BOM のないファイルを utf-8-sig で読んでも、結果は変わりませんでした('こんにちは\n')。読み込みは utf-8-sig にしておくと、BOM の有無を気にせずに済みます。 書き込みは、Excel で開く CSV なら utf-8-sig、それ以外は utf-8 が基本です。

newline引数とCRLF:改行コードの扱い

改行コードには LF(\n、Linux・macOS)と CRLF(\r\n、Windows)があります。テキストモードの open() は、newline 引数に従って改行を変換します。

読み込み時

a\r\nb\r\nc\rd\n という、改行コードが混ざったファイルを読みました。

read newline= None   ['a\n', 'b\n', 'c\n', 'd\n']
read newline= ''     ['a\r\n', 'b\r\n', 'c\r', 'd\n']
read newline= '\n'   ['a\r\n', 'b\r\n', 'c\rd\n']
read newline= '\r\n' ['a\r\n', 'b\r\n', 'c\rd\n']
newline 行の区切り 変換
None(既定) \n・\r\n・\r のすべて すべて \n に統一して渡す
"" \n・\r\n・\r のすべて 変換しない(元のまま)
"\n" \n だけ 変換しない
"\r\n" \r\n だけ 変換しない

既定のままなら、どの改行コードのファイルでも \n として読めます。 rstrip("\n") だけで改行を外せるのはこのためです。

書き込み時

"a\nb\n" を書いたときのバイト列です(Linux で実行)。

write newline= None   b'a\nb\n'
write newline= ''     b'a\nb\n'
write newline= '\n'   b'a\nb\n'
write newline= '\r\n' b'a\r\nb\r\n'
newline \n の書かれ方
None(既定) OS の改行(os.linesep)に変換。Linux・macOS は \n、Windows は \r\n(Windows の動作は公式ドキュメントで確認)
"" または "\n" 変換しない(\n のまま)
"\r\n" \r\n に変換

目的別の指定

目的 指定
ふつうのテキストを読み書き 指定なし(既定)
どの OS でも LF で書き出す open(p, "w", encoding="utf-8", newline="\n")
どの OS でも CRLF で書き出す open(p, "w", encoding="utf-8", newline="\r\n")
csv モジュールで読み書き newline=""(csv 側が改行を管理するため)

Windows で csv モジュールを使うときに newline="" を付けないと、\r\r\n となって1行おきに空行が入ることが公式ドキュメントに書かれています。CSV の読み書きは別記事「PythonでCSVを読み書き|コピペ用テンプレートと文字化け対策」で扱っています。

よく出る例外と対処(FileNotFoundError・PermissionError・IsADirectoryError)

実際に出したエラーメッセージです(PermissionError は一般ユーザー権限の Python 3.13.13 で確認、それ以外は 3.14.7)。

FileNotFoundError [Errno 2] No such file or directory: 'nai.txt'
FileNotFoundError [Errno 2] No such file or directory: 'nodir/a.txt'
IsADirectoryError [Errno 21] Is a directory: 'd'
PermissionError [Errno 13] Permission denied: 'perm.txt'
PermissionError [Errno 13] Permission denied: 'ro/new.txt'
FileExistsError [Errno 17] File exists: 'm.txt'

例外と対処の一覧表

例外 起きる場面 対処
FileNotFoundError 読むファイルがない/パスが相対パスで、実行場所が違う Path(p).resolve() で実際のパスを表示して確認
FileNotFoundError("w" なのに出る) 書き込み先のフォルダがない Path(p).parent.mkdir(parents=True, exist_ok=True)
IsADirectoryError フォルダを open() した ファイル名まで含めたパスにする
PermissionError 読み書きの権限がない/書けないフォルダ 権限と保存先を確認。Windows では、ほかのアプリで開いているファイルや、フォルダを開いた場合にもこの例外になる(公式ドキュメントで確認)
FileExistsError "x" で既存ファイルを開いた 別名にするか、上書きしてよいなら "w"
UnicodeDecodeError 読み込みの文字コードが合わない encoding を合わせる
UnicodeEncodeError 書き込み先の文字コードにない文字 encoding="utf-8" にする
ValueError: I/O operation on closed file. 閉じたあとに操作した with ブロックの中で操作する
ValueError: must have exactly one of create/read/write/append mode "rw" のようなモード "r+" にする
ValueError: binary mode doesn't take an encoding argument "rb" に encoding を付けた encoding を外す

"w" で開いているのに FileNotFoundError が出たら、ファイルではなく途中のフォルダがない、と読み替えます。 open() はファイルは作りますが、フォルダは作りません。

例外を受け取るコピペ用テンプレート

from pathlib import Path

def load(path):
    try:
        with open(path, encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        print(f"見つかりません: {Path(path).resolve()}")
    except IsADirectoryError:
        print(f"フォルダです: {path}")
    except PermissionError:
        print(f"権限がありません: {path}")
    except UnicodeDecodeError as e:
        print(f"UTF-8 ではないようです: {path}({e.reason})")
    return None

FileNotFoundError・PermissionError・IsADirectoryError はどれも OSError の仲間なので、まとめて受けたいときは except OSError as e: と書けます。e.errno と e.filename で番号とパスを取り出せます。例外処理そのものの書き方は、別記事「Python例外処理 try exceptの書き方」で扱っています。

大きなファイルは逐次読みで扱う

100万行(約 58.7 MB)のテキストを4通りで読み、tracemalloc でメモリのピークを測りました(Python 3.14.7。時間は参考値です)。

def read_all():
    with open("big.txt", encoding="utf-8") as f:
        return len(f.read().splitlines())

def readlines():
    with open("big.txt", encoding="utf-8") as f:
        return len(f.readlines())

def for_loop():
    n = 0
    with open("big.txt", encoding="utf-8") as f:
        for line in f:
            n += 1
    return n

def chunk():
    n = 0
    with open("big.txt", "rb") as f:
        while c := f.read(1024 * 1024):
            n += len(c)
    return n
読み方 メモリのピーク 時間
f.read().splitlines() 234.8 MB 0.64 秒
f.readlines() 139.9 MB 0.55 秒
for line in f: 0.2 MB 0.37 秒
f.read(1024 * 1024) を繰り返す(バイナリ) 2.2 MB 0.01 秒

for line in f: は、ファイルの大きさに関係なく1行ぶんのメモリで済み、しかも速いという結果でした。行単位で処理できるテキストは for、行の概念がないバイナリは固定サイズの read(n) を使います。

先頭だけ・末尾だけを読む

from itertools import islice
from collections import deque

with open("big.txt", encoding="utf-8") as f:
    head = list(islice(f, 2))        # 先頭2行で読み込みを止める

with open("big.txt", encoding="utf-8") as f:
    tail = list(deque(f, maxlen=2))  # 末尾2行だけを保持
['0,item-0,これは0行目のデータです\n', '1,item-1,これは1行目のデータです\n']
['999998,item-999998,これは999998行目のデータです\n', '999999,item-999999,これは999999行目のデータです\n']

読みながら別のファイルへ書き出す

with (
    open("access.log", encoding="utf-8") as src,
    open("error.log", "w", encoding="utf-8") as dst,
):
    for line in src:
        if "ERROR" in line:
            dst.write(line)   # line には改行が付いているのでそのまま書ける

読み込み側も書き込み側も1行ずつ流すので、何 GB のファイルでもメモリ使用量はほぼ一定です。

pathlibのread_text・write_textで1行にまとめる

pathlib.Path には、開く・読む(書く)・閉じるを1回でおこなうメソッドがあります。

from pathlib import Path

p = Path("p.txt")
print(p.write_text("一行目\n二行目\n", encoding="utf-8"))
print(repr(p.read_text(encoding="utf-8")))
print(p.read_text(encoding="utf-8").splitlines())

with p.open("a", encoding="utf-8") as f:
    f.write("三行目\n")
print(repr(p.read_text(encoding="utf-8")))

p.write_text("上書き\n", encoding="utf-8")
print(repr(p.read_text(encoding="utf-8")))
8
'一行目\n二行目\n'
['一行目', '二行目']
'一行目\n二行目\n三行目\n'
'上書き\n'
メソッド 相当する open() 注意点
p.read_text(encoding="utf-8") "r" で開いて read() 全体を読み込む
p.write_text(s, encoding="utf-8") "w" で開いて write() 常に上書き。追記はできない
p.read_bytes() / p.write_bytes(b) "rb" / "wb" バイナリ用
p.open("a", encoding="utf-8") open(p, "a", ...) 追記や逐次読みはこちら

read_text() も encoding を省くと既定値が使われる点は open() と同じです(先ほどの -X warn_default_encoding でも警告が出ていました)。write_text() の newline 引数は Python 3.10 以降、read_text() の newline 引数は Python 3.13 以降で使えます(公式ドキュメントで確認)。

open() と pathlib の使い分け

状況 向いている書き方
小さな設定ファイルやメモをまるごと読む・書く Path.read_text() / Path.write_text()
追記する open(p, "a") または p.open("a")
大きなファイルを1行ずつ処理する with open(p) as f: と for
書き込みながら条件で分岐する with open(p, "w") as f:

パスの連結やフォルダ作成、ファイル一覧の取得などは、別記事「Python pathlibの使い方|ファイル操作逆引き70例と実行結果」で扱っています。

JSON・CSVは専用モジュールに任せる

JSON や CSV も open() で開きますが、中身の解釈は専用モジュールに任せると、引用符やカンマのエスケープを自分で書かずに済みます。

形式 開き方 読み書き
JSON open(p, encoding="utf-8") json.load(f) / json.dump(obj, f, ensure_ascii=False, indent=2)
CSV open(p, encoding="utf-8-sig", newline="") csv.reader(f) / csv.DictReader(f) / csv.writer(f)
画像・PDF open(p, "rb") 各ライブラリに渡す

それぞれの詳しい使い方は、別記事「Python JSON 読み書き|コピペレシピと実行結果」「PythonでCSVを読み書き|コピペ用テンプレートと文字化け対策」にまとめています。この記事で扱った 「with・モード・encoding・newline」は、どの形式でも共通の土台になります。

書く前のチェックリスト

確認すること 見るところ
with を使っているか f = open(...) 単独の行がないか
encoding を書いているか テキストモードの open()・read_text()・write_text() すべて
モードは合っているか 上書きしてよいなら w、残すなら a、新規専用は x
書き込みに改行を入れたか write()・writelines() は改行を足さない
読んだ行の改行を外したか rstrip("\n") または splitlines()
保存先のフォルダはあるか Path(p).parent.mkdir(parents=True, exist_ok=True)
CSV なら newline="" を付けたか csv モジュールを使うとき
大きなファイルを read() していないか for line in f: に置き換える

よくある質問

Q. open() の encoding は、UTF-8 が既定の環境でも書いたほうがよいですか? A. 書くことをおすすめします。Python 3.14 までは OS やロケールで既定値が変わり、今回の検証でも環境変数を変えただけで同じファイルが UnicodeDecodeError になりました。Python 3.15 からは PEP 686 で UTF-8 が既定になりますが、encoding="utf-8" と書いてあれば、どのバージョンでも同じ結果になり、読む人にも意図が伝わります。

Q. with を使わずに open() したファイルは、いつ閉じられますか? A. f.close() を呼んだときです。呼ばなかった場合、CPython ではファイルオブジェクトが不要になった時点で閉じられることが多いものの、タイミングは保証されていません。今回の実行でも、write() 直後のファイルサイズは 0 バイトで、flush() または close() のあとに内容が書き出されました。with を使えば、ブロックを抜けた時点で確実に閉じられます。

Q. readlines() と for line in f: はどちらを使えばよいですか? A. 行ごとに順番に処理するなら for line in f: です。100万行のファイルでメモリのピークを測ると、readlines() は 139.9 MB、for は 0.2 MB でした。行番号で行き来したい、並べ替えたい、といった「全行をリストとして持つ必要がある」ときに readlines() や read().splitlines() を選びます。

Q. ファイルの途中の1行だけを書き換えるには、どうすればよいですか? A. テキストファイルは行の長さがまちまちなので、途中だけを差し替える操作には向いていません。全体を読み込み、リストの該当行を書き換えて、"w" で全体を書き戻す方法が確実です。大きなファイルなら、1行ずつ読みながら別のファイルへ書き出し、最後に Path.replace() で入れ替えます。

まとめ

  • 基本形は with open(パス, モード, encoding="utf-8") as f:。この3点を毎回書く
  • w は開いた時点で中身が空になる。残すなら a、新規専用なら x
  • 行ごとの処理は for line in f:。100万行でもメモリは 0.2 MB で済んだ
  • write() と writelines() は改行を付けない。\n は自分で足す
  • with なしでは、閉じるまで内容が書き出されないことがある(直後のサイズは 0 バイト)
  • encoding の既定値は環境で変わる。Python 3.15 から UTF-8 が既定(PEP 686)。書き忘れは -X warn_default_encoding で見つかる
  • BOM は utf-8-sig、Shift_JIS は cp932、CSV は newline=""

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Pythonファイル読み込み・書き込み|open・with・encoding一覧」

株式会社RJCでは、Pythonのファイル読み書きと文字コードの扱いを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?