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.
Metadata
Release files for atomic-json-io 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| atomic_json_io-0.1.0.tar.gz | 7.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| atomic_json_io-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 13.5 kB
Release files / atomic_json_io-0.1.0.tar.gz
| Download URL | atomic_json_io-0.1.0.tar.gz |
|---|---|
| Size | 7.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c8be77f199410ec7ecb4e93850ad64e76eff4a5d35a3abb67b6472cdd063aec6
|
|
BLAKE2b-256 checksum How to use checksums |
576bd518f763dd9a60351d3f4aae92b2797da576a60aad3777e572d42ebd0b0e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|
Release files / atomic_json_io-0.1.0-py3-none-any.whl
| Download URL | atomic_json_io-0.1.0-py3-none-any.whl |
|---|---|
| Size | 6.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
44c0fd7473a2c1546ce7e224e715f8e38fd602be8c904c2cfe9a976c8eb4a950
|
|
BLAKE2b-256 checksum How to use checksums |
8bbb9d0fecbb2c711d21e2dcd77d5c6a3c7abd83bbdb4bb258187353760e958b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|