完成版を書いてから名前を切り替える
既存ファイルをwモードで開いてから書き込みに失敗すると、元の内容を失うことがある。更新内容を同じフォルダーの一時ファイルへすべて書き、閉じた後でos.replace()により保存先へ切り替えれば、書き込み途中の内容を通常の読者へ見せる時間を減らせる。ここでは信頼できる作業フォルダーにある一つのテキストファイルを更新する。
原子的な置換と、停電しても保存内容が残る永続性は別である。また、複数ファイルをまとめて更新するトランザクションにもならない。この例は一時ディレクトリ内で正常な置換と、置換直前の失敗を確認する。実ファイルを更新する前にはバックアップと復旧方法を用意する。
一時ファイルを同じディレクトリに作る
example.py
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory
from unittest.mock import patch
import os
def replace_text(path, text):
path = Path(path)
if path.is_symlink():
raise ValueError("symlink destination is not supported")
temporary = None
try:
with NamedTemporaryFile(
mode="w", encoding="utf-8", newline="\n",
dir=path.parent, prefix=".write-", delete=False,
) as stream:
temporary = Path(stream.name)
stream.write(text)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
with TemporaryDirectory() as directory:
root = Path(directory)
destination = root / "result.txt"
destination.write_text("old\n", encoding="utf-8")
with patch("os.replace", side_effect=OSError("simulated replacement failure")):
try:
replace_text(destination, "incomplete update\n")
except OSError:
pass
else:
raise AssertionError("failure was not raised")
assert destination.read_text(encoding="utf-8") == "old\n"
assert list(root.glob(".write-*")) == []
print("failed update preserved:", destination.read_text().strip())
replace_text(destination, "new\n")
assert destination.read_text(encoding="utf-8") == "new\n"
assert list(root.glob(".write-*")) == []
print("successful update:", destination.read_text().strip())
実行結果
failed update preserved: old
successful update: new
flush・fsync・close・replaceの順序
write()の直後に、Python側のバッファに残っているデータをflush()で渡し、fsync()でファイルの同期を要求する。その後withを抜けてファイルを閉じ、os.replace()を呼ぶ。開いたまま置換できるかはOSや共有設定にも左右されるため、例では閉じてから切り替える順序にした。
一時ファイルをpath.parentへ作るのは、保存先と同じファイルシステム上での置換を狙うためである。別のボリュームにある一般の一時フォルダーから移そうとすると、置換が失敗することがある。os.replace()は移動先にファイルがあれば置き換える操作なので、「既存ファイルを絶対に上書きしない」目的には使わない。
finallyでは一時ファイルが残っていれば削除する。正常な置換後は一時名が存在しないためmissing_ok=Trueで扱う。例は自分が作った一時名だけを削除し、フォルダー全体の一括削除をしない。なお、一時ファイルの削除自体が権限などで失敗すれば、その例外も無視されない。実運用では元の例外と後始末の失敗の両方を記録できるとよい。
どこまで保護できるか
置換前に失敗すれば、元の保存先の内容を保持できる。例はos.replaceをmockで失敗させ、この状態を確認している。ただしOSがプロセスを強制終了した場合にはfinallyが動かず、一時ファイルが残ることがある。起動時に古い一時ファイルを整理する場合も、名前だけで無差別に削除せず、自分の処理が管理するものかを確認する。
fsync()を呼んだだけで、すべての環境の停電耐性が保証されるわけではない。ディレクトリエントリーの同期やストレージの振る舞いなども関わる。ネットワークファイルシステムの意味もローカルと同一とは限らない。強い永続性が必要な用途では、OS・ファイルシステムに合わせた手順やデータベースの利用を検討する。
一時ファイルを使うため、元ファイルのアクセス権、所有者、拡張属性などをそのまま継承するわけではない。必要なメタデータがあるなら、置換前に適切な権限で設定する設計が必要である。同じ保存先へ複数のプロセスが書けば、後の置換が先の結果を上書きする。原子的な切り替えは競合する更新の調停ではない。
リンクと並行変更の前提
例では保存先がシンボリックリンクなら拒否するが、判定後に他者が差し替える状況までは防げない。親フォルダーも含め、信頼できる場所を使う前提である。外部入力の名前を保存先へ使う場合は、基準フォルダーの外へ出ない検査や、敵対的な並行操作を考慮した別の防御が必要になる。
実行環境と関連情報
掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。
関連:作業用ファイルを残さない:TemporaryDirectoryで一時領域を管理する / 大量ファイルの名前を安全に変える:事前確認と衝突検出
- Python公式ドキュメント(2026年10月2日参照)
- Python公式ドキュメント(2026年10月2日参照)
