Python File Operations with pathlib, Context Managers, and JSON
I built safe file I/O in Python using pathlib, with context managers, and the json module, including an atomic write pattern. Writing to a temp file and renaming guaranteed config files were never left half-written on failure.
Objective & Context
Automation scripts read and rewrite config and state files constantly. This lab uses context managers for guaranteed close, pathlib for portable paths, and atomic replace to prevent corruption, the I/O backbone for the sysadmin and IR scripts.
Learning Objectives
- Navigate the filesystem with pathlib instead of string path juggling.
- Guarantee file closure with with context managers.
- Read and write JSON configs with the json module.
- Implement atomic writes via temp file plus rename.
- Verify a write round trip by reading back and comparing.
Environment & Prerequisites
Prerequisites
Python 3.11withpathlibandjson(standard library)- A writable working directory and a sample JSON config to manipulate
- Assumed skill: basic Python syntax and dicts
Step-by-Step Execution
1. Read JSON with a context manager
echo '{"name": "tyf-ai", "version": 1}' > config.jsonExpected output:
(no output; sample config.json written)import json
with open("config.json", encoding="utf-8") as f:
cfg = json.load(f)
Expected output:
(no output; cfg holds the parsed dict)
What this teaches: The with block guarantees the file is closed even if json.load raises, and explicit encoding="utf-8" avoids platform-default surprises on Windows. For one-shot reads, pathlib.Path("config.json").read_text() is the shorter equivalent.
2. Atomic write to avoid corruption
import os, tempfile, pathlib
def atomic_write(path, text):
d = pathlib.Path(path).parent
fd, tmp = tempfile.mkstemp(dir=d)
with os.fdopen(fd, "w") as f:
f.write(text); f.flush(); os.fsync(f.fileno())
os.replace(tmp, path)
Expected output:
(no output; atomic_write is defined)
What this teaches: Writing to a temp file in the same directory and then renaming makes the update atomic: readers see the old file or the new file, never a half-written one.
3. Verify the round trip
python -c "import json,pathlib; print(json.loads(pathlib.Path('config.json').read_text())['name'])"Expected output:
tyf-ai
What this teaches: Reading back the written file and extracting a known value closes the loop and proves the round trip preserved the data. This reads the config.json created in step 1, so run both steps in the same directory.
How It Works
How It Works
pathlib treats paths as objects with methods instead of strings you concatenate. The / operator joins segments portably, and methods like read_text(), write_text(), exists(), and glob() cover most file tasks without ever importing os.
Context managers exist because cleanup must happen even when code raises. The with block calls __exit__ on the way out no matter what, which closes the file handle; without it, an exception mid-write can leak handles or leave buffers unflushed.
Atomic writes exploit one filesystem guarantee: renaming a file within the same directory is atomic. By writing complete content to a temp file and then os.replace()-ing it over the target, concurrent readers and crash recovery only ever observe whole versions.
to temp file"] --> F["fsync temp file"] F --> R["os.replace(tmp, target)"] R --> DONE["readers see old
or new, never partial"]
Validation & Testing
Interrupt a write mid-stream and confirm the original file remains intact thanks to atomic replace. Pass criteria: JSON round-trips losslessly and an aborted write never corrupts the target file.
Advanced: Troubleshooting
- Partial files: never write in place; write-temp-then-replace.
- Encoding issues: specify
encoding="utf-8"explicitly. - Leaked handles: always use
withto guarantee close.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| FileNotFoundError on read | relative path resolved against the wrong working directory | build paths from pathlib.Path(__file__).parent instead of relying on cwd |
| JSONDecodeError | the file is empty, truncated, or not JSON | validate with json.tool on the command line before debugging code |
| PermissionError on rename | temp file created in a different filesystem or directory | create the temp file in the target's own directory with dir=parent |
| data lost after a crash | content sat in the OS buffer and never reached disk | call f.flush() plus os.fsync() before the rename on critical writes |
| pathlib glob finds nothing | pattern relative to the wrong base or wrong case on case-sensitive filesystems | print the resolved base path and list the directory first |
Key Results
- Guaranteed zero file corruption via atomic write-and-replace.
- Round-tripped JSON config losslessly with explicit encoding.
- Eliminated leaked file handles using context managers.
- Portable paths via pathlib across Linux and Windows.
Key Takeaways
- pathlib replaces string path juggling with readable, portable path objects.
- with blocks guarantee cleanup even when exceptions strike mid-operation.
- Atomic writes (temp file plus rename) protect configs from partial updates.
- Keep temp files in the target directory so the rename stays atomic.
- Always verify writes with a read-back round trip in scripts that matter.
Practice Challenges
Challenge 1: Config updater
Goal: Write a script that loads a JSON config, bumps a version field, and saves it atomically, then proves the old file is untouched on simulated failure.
Hint: Raise an exception between write and rename to simulate the crash; check the original survives.
Challenge 2: Safe rotator
Goal: Implement log rotation: rename app.log to app.log.1 only if it exists, using pathlib, without race-prone exists-then-act patterns.
Hint: try/except around the rename handles the missing-file case atomically.