Crash-safe atomic JSON / text / JSONL file writes for Linux (stdlib only).
Project description
atomic-json-io
Crash-safe, concurrency-safe atomic file writes for JSON, plain text, and JSONL on Linux. Pure standard library — zero runtime dependencies.
A reader of the target file always sees either the complete previous content or the complete new content, never a half-written (torn) file. Multiple threads or processes can write to the same target path at the same time without corrupting the file or racing on a shared temporary file.
Install
From PyPI:
pip install atomic-json-io
Or from source (GitHub):
pip install git+https://github.com/JohnLinotte/atomic-json-io.git
Requires Python 3.10+ and Linux.
Usage
Write JSON
from pathlib import Path
from atomic_json_io import write_json_atomic
write_json_atomic(Path("config.json"), {"name": "atomic", "version": 1})
Write text
from pathlib import Path
from atomic_json_io import write_text_atomic
write_text_atomic(Path("notes.md"), "# Title\n\nSome text")
Write JSONL
from pathlib import Path
from atomic_json_io import write_jsonl_atomic
write_jsonl_atomic(
Path("events.jsonl"),
[{"id": 1, "event": "open"}, {"id": 2, "event": "close"}],
)
All three helpers create parent directories automatically and add a trailing newline.
Why this is atomic on Linux
The write does not modify the target file in place. Instead it:
- Serializes the payload to a brand-new temporary file in the same directory as the target.
- Calls
file.flush()thenos.fsync(fd)so the bytes (and the file's data) are durably on disk before the swap. - Calls
os.replace(tmp, target)to move the temporary file onto the final path.
On Linux, os.replace is implemented with the rename(2) syscall, which the
POSIX standard guarantees to be atomic: at any instant the target name resolves
to either the old inode or the new inode, never to a partially written file. A
concurrent reader therefore opens one complete version or the other.
The fsync step matters for crash safety specifically: without it, a power loss
right after os.replace could leave the directory entry pointing at a file whose
data blocks were never written. Flushing and fsyncing before the rename closes
that window.
The cross-filesystem pitfall
rename(2) — and therefore os.replace — is only atomic when the source and the
target are on the same filesystem. That is exactly why the temporary file is
created in the same directory as the target rather than in /tmp or some other
location. A naive implementation that writes to /tmp and then "moves" the file
onto a target on a different mount would fall back to a copy-then-delete under the
hood, which is not atomic and can expose a torn file. Keeping the temporary
file as a sibling of the target guarantees a same-filesystem rename.
How concurrency is handled
Every write generates its own temporary filename containing a UUID nonce, combined with the process id and the thread id:
<target>.<pid>.<tid>.<nonce>.tmp
Because each writer owns a distinct temporary file, two simultaneous writers
never share or clobber each other's in-progress data. They each fsync their own
temp file and then race only on the final os.replace; the last rename to
complete wins, and the file is always one valid, complete payload. There is no
shared-temp-file race that could make one writer's os.replace fail with
FileNotFoundError because another writer already renamed the shared temp away.
This holds across threads (thread id + nonce) and across processes (pid + nonce).
Scope
Linux-only. There is no Windows portability claim — os.replace semantics over
an existing target differ across platforms, and the fsync-before-rename
durability argument relies on POSIX rename behavior.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file atomic_json_io-0.1.0.tar.gz.
File metadata
- Download URL: atomic_json_io-0.1.0.tar.gz
- Upload date:
- Size: 7.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8be77f199410ec7ecb4e93850ad64e76eff4a5d35a3abb67b6472cdd063aec6
|
|
| MD5 |
93d27854f747a95fe0538842adad0ab0
|
|
| BLAKE2b-256 |
576bd518f763dd9a60351d3f4aae92b2797da576a60aad3777e572d42ebd0b0e
|
File details
Details for the file atomic_json_io-0.1.0-py3-none-any.whl.
File metadata
- Download URL: atomic_json_io-0.1.0-py3-none-any.whl
- Upload date:
- Size: 6.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
44c0fd7473a2c1546ce7e224e715f8e38fd602be8c904c2cfe9a976c8eb4a950
|
|
| MD5 |
6102963877dfdf4da49ac121b910c8f6
|
|
| BLAKE2b-256 |
8bbb9d0fecbb2c711d21e2dcd77d5c6a3c7abd83bbdb4bb258187353760e958b
|