Why does os.replace raise OSError with errno EXDEV when the temp file is in /tmp?
answer
- Renaming never copies any bytes
- The two paths may not share a filesystem
- /tmp is frequently its own mount
- The error means cross-device link
- Derive the temp directory from the target
basics
~20 sA rename only rewrites a directory entry inside one filesystem; it cannot move data between filesystems. When /tmp is a separate mount from the target, os.replace fails with EXDEV. Create the temporary file in the target's own directory instead.
solid answer
~50 s`os.replace` is a rename, and a rename rebinds a name to a file that already lives in the same filesystem. It never copies bytes, so when source and destination sit on different mounts the kernel refuses with `OSError` whose `errno` is `errno.EXDEV` ("cross-device link"). `/tmp` is very often a separate mount - a memory-backed filesystem, its own partition, or a container layer distinct from the data volume - so an atomic-write helper that defaults to the system temp directory works on a laptop and fails in production. The fix is not to catch the error and copy: a copy followed by a delete is exactly the non-atomic in-place write you were avoiding. Create the temporary file in the target's parent directory - `tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(target)))` - which is by definition the filesystem the new entry must land in.
code
python · 10 linesimport os
import tempfile
target = os.path.join(tempfile.mkdtemp(), "routes.json")
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(target)))
os.close(fd)
print(os.path.dirname(tmp) == os.path.dirname(os.path.abspath(target)))
os.replace(tmp, target) # one filesystem, so this cannot fail with EXDEV
print(os.path.exists(target))go deeper
Remember one rule: the temporary file goes next to the file you are replacing, never in the system temp directory. Knowing that renames cannot cross filesystems explains why.
Be able to name the error - OSError with errno EXDEV - explain that a rename rebinds a name rather than copying bytes, and show how to derive the temp directory from the target path.
Explain why the copy-then-delete fallback silently destroys atomicity, and recognise the signature of the bug: works on a laptop, fails in a container where the data directory is a separate mount.
An interviewer expects you to treat mount layout as part of the design - which volumes hold mutable state, whether an atomic write is even possible on the target, and what that rules out, such as bind-mounting individual files.
## What a rename actually does Renaming does not touch file contents. It removes a name from one directory and adds it to another, both within the same filesystem, leaving the underlying file exactly where it was. That is why it is fast regardless of file size and why it can be atomic at all. It is also why it cannot cross a boundary: a directory entry refers to a file by an identifier that is only meaningful inside its own filesystem, so there is nowhere for the destination directory to point. The kernel does not silently upgrade the operation to a copy; it returns the `EXDEV` error, and Python raises `OSError` with `errno` set to `errno.EXDEV`. ## Why /tmp trips it so reliably On most modern Unix systems `/tmp` is not part of the root filesystem. It is commonly a memory-backed filesystem, or its own partition, or - inside a container - a mount separate from the volume where application data lives. Meanwhile the target of an atomic write is usually under a data directory on a different mount, often a network or block volume attached specifically to hold state. The two are different filesystems, so the rename cannot connect them. The symptom is characteristic and worth recognising in an interview: the helper works on a developer machine, where everything is one big filesystem, and fails immediately in production or in a container, where the data directory is a separate mount. A route-optimisation job publishing a 17-service dependency graph hits it the first time the graph file moves onto a dedicated volume, with a stack trace that names `os.replace` and an error message about a cross-device link. ## The wrong fix The tempting repair is to catch the error and fall back to copying the file over the destination, then deleting the temp file - which is what the standard library's higher-level move helper does. That is a legitimate way to *move* a file, and a completely illegitimate way to *replace* one atomically: copying over the destination truncates it and rewrites it in place, reintroducing exactly the window where a reader sees a partial file. If your fallback path is a copy, your atomic write is atomic only on machines where it was never needed. ## The right fix Derive the temporary file's directory from the target rather than from the environment: ```python import os import tempfile directory = os.path.dirname(os.path.abspath(target)) fd, tmp = tempfile.mkstemp(dir=directory) ``` The target's entry has to be created in that parent directory, so a temporary file created there is guaranteed to be on the same filesystem. This is stronger than comparing device numbers with `os.stat`, and it needs no probing or configuration. Two practical notes: the parent directory must already exist, because `mkstemp` will not create it; and if the target path is a symbolic link, resolving it first decides whether you replace the link or the file it points at - both are defensible, but the choice should be deliberate. ## os.rename versus os.replace Both are renames and both raise `EXDEV` across filesystems. They differ on an existing destination: `os.replace` overwrites it on every platform, while `os.rename` overwrites on Unix but raises `FileExistsError` on Windows, where renaming onto an existing name is not permitted. That is why the atomic-write recipe always specifies `os.replace` - a helper written with `os.rename` and tested only on Linux breaks the first time it runs on Windows, and the failure looks nothing like a filesystem problem. ## Windows and other edges On Windows the equivalent boundary is the drive letter: replacing a file on `D:` with a temporary file on `C:` fails for the same structural reason. Windows adds a failure mode Unix does not have - if another process holds the destination open without permitting deletion, the replace fails with a permission error rather than succeeding - so services that update files other processes read often wrap the swap in a short retry. And on Unix, a target that is itself a bind-mounted single file cannot be replaced by a rename at all, because the name and the mount are pinned together; that is a deployment shape to avoid for files you intend to update.
- How does os.replace differ from os.rename when the destination already exists?`os.replace` overwrites the destination silently on every platform. `os.rename` does the same on Unix, but on Windows it raises `FileExistsError`, because renaming onto an existing name is not allowed there. That difference is the whole reason the atomic-write recipe names `os.replace`: code written with `os.rename` and tested only on Linux breaks on Windows in a way that looks unrelated to file replacement.
- Is os.replace atomic on Windows as well?It is a single call that either replaces the name or fails, so a reader never observes a partial file. It is more fragile than a Unix rename though: if another process holds the destination open without allowing deletion, the call fails with a permission error instead of succeeding. Services that replace files other processes keep open usually wrap the swap in a bounded retry.
- How do you pick the temp directory for an arbitrary target path?Take the target's parent - `os.path.dirname(os.path.abspath(path))` - and pass it to `tempfile.mkstemp(dir=...)`. Same directory means same filesystem by construction, which is stronger than comparing device numbers from `os.stat` and needs no configuration. Create the parent first if it may not exist, since `mkstemp` will not create it for you.
saying these in an interview costs you the question
- Thinks a rename can move bytes between filesystems
- Falls back to copy-then-delete and still calls it atomic
- Assumes the system temp directory shares the target's device
- Hardcodes a temp directory instead of deriving it
- Believes os.rename and os.replace behave identically everywhere