Skip to content
Python Basics: Replace a Complete File Without Exposing a Partial Write

Python Basics: Replace a Complete File Without Exposing a Partial Write

October 29, 2026

Writing directly to a configuration path can expose an incomplete file to another reader. In a private scratch directory, write a complete sibling first and then replace the destination. An existing reader demonstrates why replacement changes the pathname rather than every open handle.

Prepare a scratch directory

Tested on Linux with CPython 3.12.14. You need Bash, Python, a text editor, and the standard mktemp, rm, and rmdir utilities. Run without -O so the assertions remain enabled. No external packages are required.

Keep the same terminal open. Stop if setup fails, before saving the source:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Run the example

Save this as demonstrate-atomic-file-replacement.py:

from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as directory:
    target = Path(directory) / "settings.txt"
    staged = target.with_name("settings.next")
    target.write_text("mode=old\n", encoding="utf-8")

    with target.open(encoding="utf-8") as old_reader:
        with staged.open("x", encoding="utf-8") as writer:
            writer.write("mode=new\n")
        staged.replace(target)

        current = target.read_text(encoding="utf-8").strip()
        previous = old_reader.read().strip()
        assert current == "mode=new"
        assert previous == "mode=old"
        assert not staged.exists()
        print("New open:", current)
        print("Existing handle:", previous)
        print("Staged path exists:", staged.exists())

Run it:

python3 -B demonstrate-atomic-file-replacement.py

Observed output:

New open: mode=new
Existing handle: mode=old
Staged path exists: False

Read the result

The exclusive "x" open refuses to overwrite an unexpected staged file. Closing the writer finishes Python’s buffered write before replacement. Because the staged file is beside the destination, both paths belong to the same filesystem in this fixture.

On Linux, a successful replacement switches the destination name atomically. A new open sees the new file. The handle opened earlier still refers to the old file, even though that file no longer has the destination name. The temporary directory removes both fixture files after their handles close.

Apply it carefully

Atomic visibility is different from crash durability. This example does not call fsync() on the staged file and directory, so it makes no power-loss guarantee. Replacement also installs the staged file’s metadata; it does not preserve the old file’s permissions or ownership automatically. A shared destination needs a unique staging name, controlled directory permissions, failure cleanup, and a policy for competing writers. Do not apply this fixed-name fixture to an untrusted directory.

The official pathlib and os.replace reference describes the relevant APIs and their limits.

Clean up

Remove the saved source, return to the starting directory, and remove the empty practice directory:

rm -- demonstrate-atomic-file-replacement.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If the directory is not empty, inspect its remaining contents before removing anything else.

Acknowledgements

This post was created with assistance from AI.

Last updated on