← back to the archiveCover illustration for “Atomic file replacement needs a durability check”
TILday 111·4w ago·Published · Updated ·by Andy Padia

Atomic file replacement needs a durability check

In short: Atomic file replacement prevents partial-file visibility, but crash persistence, concurrent writers and metadata need separate storage checks.

python ▸ import os
import tempfile
from pathlib import Path

def replace_text(destination, text):
    destination = Path(destination)
    fd, temporary = tempfile.mkstemp(
        dir=destination.parent, prefix=".result-"
    )
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as output:
            output.write(text)
            output.flush()
            os.fsync(output.fileno())
        os.replace(temporary, destination)
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)

Python added os.replace in version 3.3, whose first release arrived on 29 September 2012. The operation provides an atomic rename when it succeeds, with the usual filesystem boundary constraints. SQLite's atomic-commit explanation goes further into the hard part: operating systems and storage can buffer and reorder writes, so surviving a crash requires more than a convincing filename.

Atomic file replacement needs a durability check when a result is supposed to survive power loss. My narrower starting rule is to separate complete-file visibility from persistence. A reader seeing either the old file or the new one is useful. It does not establish what a reboot will reveal.

Replace the complete file in the same directory

The Python operating-system interface supplies the replacement primitive. Write a temporary sibling, finish it, then replace the destination. Keeping both paths in the same directory avoids deliberately crossing filesystems.

Here is a small helper for a trusted local result path. It synchronises the temporary file before replacement and removes an unused temporary file on failure. It does not claim to be a complete crash-durable storage protocol.

import os
import tempfile
from pathlib import Path

def replace_text(destination, text):
    destination = Path(destination)
    fd, temporary = tempfile.mkstemp(
        dir=destination.parent, prefix=".result-"
    )
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as output:
            output.write(text)
            output.flush()
            os.fsync(output.fileno())
        os.replace(temporary, destination)
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)

The destination must already have an existing parent directory. This helper is not a permission-preserving editor: the replacement inherits the temporary file's mode rather than the old file's metadata. Decide whether that is appropriate for your artifact before using it.

Atomic visibility is only one storage promise

The SQLite atomic-commit document is worth reading even if you are not building a database. Its explanation distinguishes application writes from what storage has durably accepted and describes the assumptions needed for recovery.

In this helper, synchronising file contents does not by itself prove that the directory-entry replacement will survive a sudden outage. Directory synchronisation and platform-specific guarantees need separate consideration. Network filesystems introduce another contract to inspect rather than inherit by assumption.

There is also a multi-writer issue: two workers can each produce a complete file and replace the same destination. The last replacement wins. Atomic replacement is not a compare-and-swap operation, a lock or a version conflict detector.

The synthetic replacement check

A local Python 3.14.6 fixture first wrote an old result, then used the helper to install complete new text. A second check injected an OSError at the replacement step. The accepted destination remained unchanged, the error propagated and the unused temporary file was removed.

These checks exercise successful replacement and a failure before the rename. No power-loss test, concurrent-reader experiment, network-filesystem test or cross-platform certification was performed. The sample demonstrates a useful boundary, not a storage durability guarantee.

Choose storage from the cost of loss

For a hypothetical agent that writes a JSON report, I would serialise and validate the report before calling this helper. A complete invalid report is still invalid. I would then decide whether the artifact is reproducible scratch output or an authoritative record whose loss would matter.

That distinction changes the storage choice. A regenerable report may tolerate a narrowly documented local-file contract. A record of an irreversible external action deserves transactional storage, an operation identity and recovery behaviour tested against the actual failure model.

What's in it for you

  • Test failure before replacement without damaging the last accepted result.
  • Specify visibility, concurrent-writer behaviour and crash persistence separately.

An atomic rename closes the partial-file window; durability needs its own evidence.

#python#storage

Sources

  1. docs.python.org/3.12/library/os.html
  2. sqlite.org/atomiccommit.html
  3. python.org/downloads/release/python-330
← older drop
Record/replay keeps fast-moving AI API tests honest
newer drop →
Local AI-agent observability needs a rollback path

related drops

explore all 402 drops →
← back to the archiveday 139