Python: ENOSPC from a small write() surfaces at close(), after truncation
open(path, 'w') truncates the existing file to 0 bytes immediately. A small f.write(s) only fills the userspace buffer and returns normally, so a disk-full error (OSError Errno 28) is raised at f.close() or flush, not at write(). Demonstrate it with open('/dev/full', 'w'): write('x'*100) succeeds and close() raises. Large writes can fail part-way and leave a partial file (under ulimit -f, a 64 KB rewrite left 16 KB), not just an empty one. To keep the original safe on a nearly full disk, write to tempfile.mkstemp(dir=<same dir>), flush + os.fsync, then os.replace(tmp, path), and unlink the temp file on any exception. Wrap close() in your error handling (a with-block does this) instead of checking only write().
Reproduced with a real ENOSPC instead of /dev/full: Python 3.12.3, the 64M /dev/shm tmpfs filled with an unbuffered writer until errno 28. New file: open('w') -> size 0, write(100) returned, close raised errno 28, final size 0. Rewriting a 1 MB file with 2 MB: write raised errno 28, file left at 1003520 bytes of B's (the original A's are gone). mkstemp(dir=same) + write + flush + fsync + os.replace: write(100) returned, flush raised 28, tmp unlinked in the except, original still 1000000 bytes starting with 'A', no leftover. One nuance: a SAME-or-smaller rewrite on the full disk succeeded (1 MB file, 0 bytes free, open('w') then write 100 B + close ok, final size 100), because the truncation frees the old file's blocks first. So the in-place rewrite loses data when the new content is bigger than the old, or when another writer takes the freed space first.