Skip to content
VLSI Mentor

Python Part I · Module 15 · File Handling

Creating, Renaming and Deleting Files

Three operations that change the filesystem rather than reading it, which makes them categorically different from everything else in this module: a wrong read produces a wrong answer you can correct, and a wrong delete produces nothing at all. The mechanics are three method calls. The reason this chapter exists is that these calls will eventually run inside a script nobody is watching, at an hour nobody is awake, against paths assembled from strings that came from somewhere else — and one of the three does something quietly destructive that almost nobody expects.

Foundation15 min readPythonPart IFundamentalsFilesAutomation

Part I · Module 15 · Chapter 15.9 · Creating, Renaming and Deleting Files

1. Changing the Filesystem

Everything until now either read a file or wrote one you named. This chapter creates, moves and removes — operations with a property reads do not have: if you get them wrong, there may be nothing left to inspect.

The canonical sequence:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
from pathlib import Path

p = Path("report.txt")
p.write_text("done")        # create
p.rename("report.old")      # move
p.unlink(missing_ok=True)   # remove

Four lines, three irreversible operations, and one of them behaves differently from how it reads. Taken in order.

2. Creating

A file comes into existence the moment something opens it for writing:

Azvya Education Pvt. Ltd.VLSI Mentor
create.py
from pathlib import Path

p = Path("report.txt")
print("before:", p.exists())

p.write_text("done\n")

print("after :", p.exists(), p.stat().st_size, "bytes")
print("read  :", repr(p.read_text()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before: False
after : True 5 bytes
read  : 'done\n'

write_text is the short form of the with block from Chapter 15.6 — it opens in "w", writes, and closes. read_text is the short form of Chapter 15.2's read.

Both handle the whole file at once, so Chapter 15.7's rule applies without modification: use them for small files you control — a status file, a seed, a short report — and never for a simulation log.

They also inherit "w" semantics, including the one that matters:

Azvya Education Pvt. Ltd.VLSI Mentor
clobber.py
from pathlib import Path

p = Path("report.txt")
p.write_text("last night's results\n")
p.write_text("tonight\n")
print(repr(p.read_text()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
'tonight\n'

write_text replaces. There is no append form; for that, use open(path, "a") as Chapter 15.6 did.

3. Creating Only If Absent

When overwriting would be a bug, say so — "x" mode from Chapter 15.6, which has no Path shorthand:

Azvya Education Pvt. Ltd.VLSI Mentor
exclusive.py
from pathlib import Path

Path("report.txt").write_text("existing\n")

for attempt in ["report.txt", "fresh.txt"]:
    try:
        with open(attempt, "x") as f:
            f.write("created by this run\n")
        print(f"{attempt:<12} created")
    except FileExistsError:
        print(f"{attempt:<12} already exists -- refusing to overwrite")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
report.txt   already exists -- refusing to overwrite
fresh.txt    created

This is the cheapest guard in the module. A run directory that should be fresh, a result file that should be written once — "x" turns a silent clobber into a loud failure.

4. Renaming Silently Overwrites

The fact worth the price of the chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
rename.py
from pathlib import Path

Path("report.old").write_text("YESTERDAY -- the one good run this week\n")
Path("report.txt").write_text("today\n")

print("before:")
print("  report.old :", repr(Path("report.old").read_text()))
print("  report.txt :", repr(Path("report.txt").read_text()))

Path("report.txt").rename("report.old")

print("after:")
print("  report.old :", repr(Path("report.old").read_text()))
print("  report.txt exists:", Path("report.txt").exists())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before:
  report.old : 'YESTERDAY -- the one good run this week\n'
  report.txt : 'today\n'
after:
  report.old : 'today\n'
  report.txt exists: False

Yesterday's report is gone. rename did not refuse, did not warn, and did not return anything to check. The destination existed and was replaced.

This breaks the common "rotate the old one aside" idiom:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
p.rename("report.old")      # destroys any existing report.old

Run that nightly and report.old is never older than one day — the rotation you thought you had does not exist.

The guarded version:

Azvya Education Pvt. Ltd.VLSI Mentor
safe_rotate.py
from pathlib import Path

Path("report.old").write_text("YESTERDAY\n")
Path("report.txt").write_text("today\n")

dest = Path("report.old")
if dest.exists():
    print("refusing to rotate: report.old already exists")
else:
    Path("report.txt").rename(dest)

print("report.old still:", repr(dest.read_text()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
refusing to rotate: report.old already exists
report.old still: 'YESTERDAY\n'

5. Deleting, and missing_ok

Azvya Education Pvt. Ltd.VLSI Mentor
unlink.py
from pathlib import Path

p = Path("scratch.txt")
p.write_text("temporary\n")
print("exists:", p.exists())

p.unlink()
print("after unlink:", p.exists())

try:
    p.unlink()
except FileNotFoundError as e:
    print("second unlink:", type(e).__name__)

p.unlink(missing_ok=True)
print("with missing_ok: no error")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exists: True
after unlink: False
second unlink: FileNotFoundError
with missing_ok: no error

unlink() removes a file. On a path that is not there it raises, which is usually not what a cleanup step wants:

Azvya Education Pvt. Ltd.VLSI Mentor
cleanup.py
from pathlib import Path

for name in ["a.tmp", "c.tmp"]:
    Path(name).write_text("x\n")

removed = 0
for name in ["a.tmp", "b.tmp", "c.tmp"]:
    Path(name).unlink(missing_ok=True)
    removed += 1

print("cleanup attempted on:", removed, "paths")
print("remaining:", [n for n in ["a.tmp", "b.tmp", "c.tmp"] if Path(n).exists()])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cleanup attempted on: 3 paths
remaining: []

Without missing_ok=True, b.tmp — already absent — would raise and c.tmp would never be removed. A cleanup loop that aborts on the first already-clean item leaves the rest dirty, which is the opposite of what it was written for.

unlink removes files only. A directory raises IsADirectoryError, and directory removal is Module 18 Chapter 18.9's subject, deliberately — removing a tree is a different risk.

6. Why Deletion Is Different

Three properties, none of which apply to a wrong read:

  1. No undo. There is no recycle bin. The bytes are gone, and on a scratch filesystem there is usually no backup either.
  2. No prompt. rm asks sometimes; unlink() never does. There is no interactive safety net because there is no interaction.
  3. It runs unattended. A nightly regression executes this code at 3am against paths built from a config file that someone edited yesterday.

The third amplifies the first two. A delete written for runs/seed_41 and executed with an empty seed becomes a delete of runs/ — and nobody is watching.

7. Three Countermeasures

Name it precisely, and check what you built. A path assembled from parts can collapse if a part is empty:

Azvya Education Pvt. Ltd.VLSI Mentor
collapse.py
from pathlib import Path

for seed in ["41", ""]:
    target = Path("runs") / f"seed_{seed}" / "sim.log"
    print(f"seed={seed!r:<5} -> {target}")
    print(f"           parts={len(target.parts)} name={target.name!r}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
seed='41'  -> runs/seed_41/sim.log
           parts=3 name='sim.log'
seed=''    -> runs/seed_/sim.log
           parts=3 name='sim.log'

Here the empty seed produced seed_ rather than something catastrophic, because the f"seed_{seed}" wrapper kept a non-empty component. That is the defence: never let an interpolated value be an entire path component on its own.

Delete only what you created. A script that removes files it wrote knows their names. A script that removes everything matching a pattern is trusting a pattern:

Azvya Education Pvt. Ltd.VLSI Mentor
ownlist.py
from pathlib import Path

written = []
for i in range(3):
    p = Path(f"tmp_{i}.txt")
    p.write_text("x\n")
    written.append(p)

Path("do_not_touch.txt").write_text("someone else's\n")

for p in written:
    p.unlink(missing_ok=True)

print("removed:", len(written))
print("survivor exists:", Path("do_not_touch.txt").exists())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
removed: 3
survivor exists: True

Prefer moving aside to removing. A rename into a quarantine name is reversible for as long as you leave it there, and Section 4's guard makes it safe:

Azvya Education Pvt. Ltd.VLSI Mentor
aside.py
from pathlib import Path

Path("suspect.log").write_text("possibly wanted\n")

src, dest = Path("suspect.log"), Path("suspect.log.discarded")
if dest.exists():
    print("quarantine slot occupied -- leaving suspect.log alone")
else:
    src.rename(dest)
    print("moved aside:", dest.name, "->", repr(dest.read_text()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
moved aside: suspect.log.discarded -> 'possibly wanted\n'

8. touch() and the Sentinel File

touch() creates a file if it is absent and, crucially, leaves it alone if it is not:

Azvya Education Pvt. Ltd.VLSI Mentor
touch.py
from pathlib import Path

d = Path("done.marker")
print("before :", d.exists())

d.touch()
print("after  :", d.exists(), d.stat().st_size, "bytes")

d.touch()
print("again  :", d.exists(), "-- still there")

keep = Path("has.txt")
keep.write_text("keepme\n")
keep.touch()
print("content:", repr(keep.read_text()))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before : False
after  : True 0 bytes
again  : True -- still there
content: 'keepme\n'

touch() does not truncate. That is the difference from write_text(""), which empties an existing file — compare Section 2. Touching a file that already has content updates its timestamp and leaves every byte in place.

The empty file it creates is the sentinel pattern, and it is everywhere in flow automation: a zero-byte done.marker next to a run directory means that run completed. The attraction is that the check costs nothing and cannot be misread — a file either exists or does not, with no content to parse:

Azvya Education Pvt. Ltd.VLSI Mentor
sentinel.py
from pathlib import Path

Path("run_41").mkdir()
Path("run_42").mkdir()
Path("run_41/done.marker").touch()


def finished(run):
    return (Path(run) / "done.marker").exists()


for run in ["run_41", "run_42"]:
    print(f"{run}: {'complete' if finished(run) else 'INCOMPLETE or still running'}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
run_41: complete
run_42: INCOMPLETE or still running

This is Chapter 15.10's end-marker idea moved out of the log and into the filesystem, and it is more robust for exactly one reason: the marker is written last, by a step that only runs on success. A log's end marker can be absent because the tool was killed; a sentinel is absent for the same reason, but it cannot be accidentally present, because nothing else creates it.

The rule that makes it work:

Create the sentinel as the final action, after everything it attests to has completed.

A sentinel written at the start of a run attests to nothing at all.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
predict.py
from pathlib import Path

a = Path("a.txt")
b = Path("b.txt")
a.write_text("AAA\n")
b.write_text("BBB\n")

print("A:", a.exists(), b.exists())

a.rename(b)

print("B:", a.exists(), b.exists())
print("C:", repr(b.read_text()))

b.unlink(missing_ok=True)
b.unlink(missing_ok=True)

print("D:", b.exists())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: True True
B: False True
C: 'AAA\n'
D: False

B — a.txt is gone. A rename is a move, not a copy; there is no second file afterwards.

C is the destructive one. b.txt now holds AAA. The BBB that was there was overwritten with no error, no warning and no return value to inspect. If b.txt had been the only copy of something, it is gone.

D — unlink(missing_ok=True) twice is fine. The second call had nothing to do and said nothing about it, which is what makes cleanup loops writable.

10. Exercises

Exercise 1 — create and read. Use write_text and read_text on a small file. Then say why you would not use them on a 14 GB log.

Exercise 2 — refuse to clobber. Write the same path twice with write_text and record what survives. Then prevent it with "x" mode.

Exercise 3 — the rename trap. Create two files and rename one onto the other. Confirm the destination's contents are gone and that nothing raised.

Exercise 4 — guarded rotation. Write a rotate step that refuses when the destination exists. Run it twice and show the second run changes nothing.

Exercise 5 — missing_ok. Write a cleanup loop over three names where the middle one is absent, without missing_ok. Record which files survive, then fix it.

Exercise 6 — directories. Call unlink() on a directory and record the error. Say which chapter handles that case.

Exercise 7 — the empty component. Build a path from an interpolated value that is empty, and print .parts. Describe a wrapper that makes it safe.

Exercise 8 — three countermeasures. For a script that deletes last week's run directories, write one sentence each on how you would apply all three of Section 7's measures.

11. Summary

  • These operations change the filesystem, so a mistake may leave nothing to inspect.
  • write_text and read_text are whole-file shorthands for a with block — fine for small files, never for a log.
  • write_text replaces; there is no append form.
  • "x" mode refuses to overwrite and is the cheapest guard available.
  • rename silently overwrites its destination — the same property that makes os.replace useful for atomic publishing makes rotation destructive.
  • Guard a rename with an exists() check when the destination matters.
  • unlink() raises on a missing path; missing_ok=True is what lets a cleanup loop finish.
  • A cleanup loop that aborts on the first already-clean item leaves the rest dirty.
  • Deletion has no undo, no prompt, and runs unattended — the third amplifies the first two.
  • touch() creates without truncating, unlike write_text("").
  • A zero-byte sentinel file written last is the cheapest possible record that a run completed — and it must be the final action to mean anything.
  • Three countermeasures: never let an interpolated value be a whole path component, delete only what you created, and prefer moving aside to removing.

The last chapter of the module puts the whole of it to work on the question a regression script actually has to answer.

Where this fits

Part of the Python curriculum.