Python Tutorial / Search and Copy Files with a Dry Run and Verification
Start batch file automation by separating a preview of intended changes from the apply step. This exercise copies text files into a new location and refuses to overwrite an existing destination.
Define the target and preservation policy
Use a fresh practice folder. The example works under copy-demo/in and copy-demo/out, does not recursively search subfolders and excludes symbolic-link entries. It preserves source files rather than moving or deleting them.
Moving affects the source as well as the destination. Separating copy, content verification and later cleanup makes failures easier to assess. This example stops on a name collision instead of silently inventing a new filename.
For the relevant APIs and platform requirements, see Python: shutil.
Separate sample preparation from copying. In an empty practice folder run setup_copy.py once to create copy-demo/in/note.txt. Preparation writes this fixture; the copy tool’s default dry run only displays paths and creates no copy-demo/out directory or file. Only --apply copies, and an existing destination stops the operation. This exercise copies without moving or deleting the source.
Save the complete example as setup_copy.py and run python setup_copy.py in the same practice directory.
from pathlib import Path
source = Path("copy-demo/in")
source.mkdir(parents=True, exist_ok=True)
with (source / "note.txt").open("x", encoding="utf-8") as f:
f.write("demo\n")
print("sample ready")
After seeing sample ready, run safe_copy.py.
Destination paths and failure handling build on the rules demonstrated in Handle Exceptions with try, except, else, finally and raise.
Inspect the default dry run
Save safe_copy.py as UTF-8 and complete the separate preparation step first.
import argparse
import hashlib
import shutil
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument("--apply", action="store_true")
args = parser.parse_args()
source, target = Path("copy-demo/in"), Path("copy-demo/out")
if not source.is_dir():
parser.error("prepare copy-demo/in first")
files = sorted(p for p in source.glob("*.txt") if p.is_file() and not p.is_symlink())
for src in files:
dst = target / src.name
if dst.exists() or dst.is_symlink():
raise FileExistsError(dst)
print("copy", src.name)
if args.apply:
target.mkdir(parents=True, exist_ok=True)
with src.open("rb") as incoming, dst.open("xb") as outgoing:
shutil.copyfileobj(incoming, outgoing)
if hashlib.sha256(src.read_bytes()).digest() != hashlib.sha256(dst.read_bytes()).digest():
raise RuntimeError("copy verification failed")
print("applied" if args.apply else "dry-run")
Initial output:
copy note.txt dry-run
Apply the reviewed copy and verify the bytes
After reviewing the target list, execute:
python safe_copy.py --apply
The first apply prints copy note.txt and applied, and creates copy-demo/out/note.txt. Opening the target with xb rejects an existing file. SHA-256 comparison checks that the small example’s source and destination bytes agree.
A second apply should raise FileExistsError and preserve the prior result. A failed copy can leave a partial destination; inspect its resolved path and content before deciding whether it is safe to remove and retry.
Know the limits before expanding the tool
The hash implementation reads each small file into memory. Large files should be hashed in chunks. Byte equality does not verify access-control rules, all metadata or symbolic-link semantics, and this example does not defend against an untrusted user concurrently replacing parent directories.
Before adding renaming or deletion, design target inventories, collision detection, backups and recovery. For cleanup, verify that resolved absolute targets remain inside the disposable copy-demo directory and remove only the intended results. The pathlib lesson explains how working-directory choices affect this scope.




