Python Central HubSign in

Automation and Scripting

Safety Warning (Dry Runs and Backups)

Learn safe automation habits - dry runs, backups, logging, and guardrails to avoid accidental data loss.

diagram the dry-run pattern, and why it is not optional for a bulk script mermaid
A script that renames or deletes across a directory tree has no undo. The shape that makes it safe is a single flag threaded to one place: the code decides what it would do, prints it, and only acts when told to. The default should be the harmless one, so a script run by accident does nothing.

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.

Python
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)

Python
import logging
 
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s"
)
 
logging.info("starting job")

Add confirmations for destructive actions

Python
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.

Python
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() and Path("/") are easy to compare.
  • Move to a trash folder instead of deleting. Deleting later is easy, while restoring is not. The send2trash package 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 --apply flag 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

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Check your understanding

quizCheck yourself
  1. What should the default behaviour of a bulk-delete script be?

    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.

  2. Why is an input() confirmation unsuitable for a scheduled job?

    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.

  3. Which is the safest place to keep a backup before a bulk change?

    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.

Was this page helpful?

Let us know how we did