From 20bbdb6d553590737415fc4ae9b42341a185e512 Mon Sep 17 00:00:00 2001 From: jbranchaud Date: Mon, 22 Jun 2026 17:26:14 -0500 Subject: [PATCH] Add Make Secure Temp File For Atomic Write as a Python TIL --- README.md | 3 +- .../make-secure-temp-file-for-atomic-write.md | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 python/make-secure-temp-file-for-atomic-write.md diff --git a/README.md b/README.md index 09a7a24..92d4468 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/). For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter). -_1798 TILs and counting..._ +_1799 TILs and counting..._ See some of the other learning resources I work on: @@ -1079,6 +1079,7 @@ If you've learned something here, support my efforts writing daily TILs by - [Load A File Into The Python REPL](python/load-a-file-into-the-python-repl.md) - [Look Inside Pytest tmp_path](python/look-inside-pytest-tmp-path.md) - [Make Dataclass Sortable By Specific Field](python/make-dataclass-sortable-by-specific-field.md) +- [Make Secure Temp File For Atomic Write](python/make-secure-temp-file-for-atomic-write.md) - [Override The Boolean Context Of A Class](python/override-the-boolean-context-of-a-class.md) - [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md) - [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md) diff --git a/python/make-secure-temp-file-for-atomic-write.md b/python/make-secure-temp-file-for-atomic-write.md new file mode 100644 index 0000000..1b12d4d --- /dev/null +++ b/python/make-secure-temp-file-for-atomic-write.md @@ -0,0 +1,48 @@ +# Make Secure Temp File For Atomic Write + +Two types of failure modes that can occur while writing to a shared file on the +file system are 1) a corrupted file due to a crash mid-write and 2) another +process reading a partial file mid-write. + +One way I've handled this in [`py-vmt`](https://github.com/jbranchaud/py-vmt) is +to perform the write operations on a secure temp file and then use the OS-level +atomic `rename` operation. I do this by [creating a +`contextmanager`](https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager) +that uses +[`tempfile.mkstemp`](https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp) +and [`os.replace`](https://docs.python.org/3/library/os.html#os.replace). + +Here is what the `contextmanager` looks like: + +```python +from contextlib import contextmanager +from pathlib import Path +import os, tempfile + +@contextmanager +def atomic_write(path: Path): + # write to a tmp file in the same directory, then atomically swap it + fd, temp_file_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp") + try: + with os.fdopen(fd, "w") as file: + yield file + os.replace(temp_file_path, path) + except BaseException: + os.unlink(temp_file_path) + raise +``` + +This explicitly creates a secure temp file in the same directory as the given +path with `.tmp` as the suffix. I then open the file descriptor using the +`os.fdopen` context manager (which will manage closing the file descriptor for +me). The `@contextmanager` decorator plus the `yield file` are what allow this +to be used as a `with` block. Once any file operations are done, then I use +`os.replace` to atomically swap out the original file with the temp file. + +Here is how I use it to write updates to JSON data files: + +```python +def write_active_session(self, session: Session) -> None: + with atomic_write(self.active_session_file) as file: + json.dump(session.marshal(), file) +```