flowchart TD
A["build the list of planned actions"] --> B["print every one"]
B --> C{"--apply passed?"}
C -->|no, the default| D["stop here -- nothing was changed"]
C -->|yes| E{"is there a backup?"}
E -->|no| F["make one first"]
E -->|yes| G["perform the actions"]
F --> G
G --> H["log what was actually done, not what was planned"]
D --> I["read the output, then re-run with --apply"]
Why this matters
Automation can:
- delete thousands of files instantly
- overwrite important documents
- send emails/messages to the wrong people
You want guardrails.
Dry run pattern
A dry run prints what would happen without doing it.
from pathlib import Path
def delete_tmp_files(folder: Path, dry_run: bool = True):
for path in folder.rglob("*.tmp"):
if dry_run:
print("[DRY RUN] would delete", path)
else:
print("deleting", path)
path.unlink()
delete_tmp_files(Path("./demo"), dry_run=True)Use a sandbox folder
- Copy a small sample dataset
- Run scripts there first
Always keep backups
At minimum:
- zip the folder before modifying it
- store backups outside the working directory
Add logging (not print)
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
logging.info("starting job")Add confirmations for destructive actions
from pathlib import Path
folder = Path("./demo")
ans = input(f"Really delete all .tmp files under {folder}? (yes/no) ")
if ans.strip().lower() != "yes":
raise SystemExit("Cancelled")A reusable safe pattern
The dry-run diagram at the top of the page has three parts: build the plan, show it, and act only on request. Put those in a small function and reuse it in every script that changes things. The plan is a list of (source, destination) pairs, and nothing touches the disk until the last step.
import shutil
from pathlib import Path
def plan_moves(folder: Path, dest: Path, ext: str) -> list[tuple[Path, Path]]:
return [(p, dest / p.name) for p in sorted(folder.glob(f"*{ext}"))]
def apply_moves(plan: list[tuple[Path, Path]], apply: bool = False) -> None:
for src, dst in plan:
if dst.exists():
print("SKIP (target exists):", dst) # never overwrite silently
continue
if not apply:
print("[DRY RUN]", src, "->", dst)
continue
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dst))
print("moved", src, "->", dst)
plan = plan_moves(Path("./demo"), Path("./demo/archive"), ".txt")
apply_moves(plan, apply=False)The default is apply=False, so calling it without thinking changes nothing. It never overwrites an existing target, and it prints what it actually did. Because the plan is a plain list, you can also count it, and refuse to continue if it is suspiciously large: if len(plan) > 500: raise SystemExit("too many files, check the pattern").
Guardrails to add
- A limit. A wrong pattern, such as
*instead of*.tmp, can match everything. Cap the number of files per run. - A path check. Refuse to run on a folder such as your home directory or the drive root.
Path.home()andPath("/")are easy to compare. - Move to a trash folder instead of deleting. Deleting later is easy, while restoring is not. The
send2trashpackage sends files to the operating system's recycle bin. - Backups first. Zip the folder before a bulk change (see the backups lesson), and confirm the archive exists before continuing.
- Real confirmation. The
input()prompt shown above is right for interactive tools. It cannot be used in scheduled jobs, so use an explicit--applyflag there. - Log the truth. Log what was really done, not what was planned. They differ when something fails halfway.
- Test on copies. A folder of twenty sample files finds most bugs at no cost.
For scripts that message people
The same idea applies to email, chat, and SMS, where a mistake cannot be undone. Send to yourself first. Print the recipients and the message, and require an explicit send flag. Limit how many messages a run may send.
Common mistakes
- Making the dangerous action the default.
- Testing on real data "just this once".
- Building the list while deleting, so that a change alters the list mid-loop. Build the plan first.
- No backup outside the working folder, so one wrong command removes both.
- Trusting a relative path. A script started from a different folder acts on different files. Resolve to absolute paths and print them.
๐งช Try It Yourself
Exercise 1 โ A Dry-Run Function
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Check your understanding
What should the default behaviour of a bulk-delete script be?
A script run by accident should do nothing harmful. The dangerous path should need an explicit opt-in such as --apply.
Show answer
B โ Only print what it would do, and require an explicit flag to act โ A script run by accident should do nothing harmful. The dangerous path should need an explicit opt-in such as --apply.
Why is an input() confirmation unsuitable for a scheduled job?
Unattended jobs cannot answer prompts. Use a flag such as --apply, plus limits and backups.
Show answer
A โ There is nobody to type, so the job would hang, and an explicit flag should be used instead โ Unattended jobs cannot answer prompts. Use a flag such as --apply, plus limits and backups.
Which is the safest place to keep a backup before a bulk change?
A backup inside the folder can be changed or deleted by the same mistake. Keep it apart.
Show answer
B โ Outside the working folder, ideally on another disk โ A backup inside the folder can be changed or deleted by the same mistake. Keep it apart.