Backup strategy (simple and effective)
- choose what to back up
- create timestamped archives
- store backups in a separate location
Zip a folder
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import shutil
def backup_folder(folder: Path, out_dir: Path) -> Path:
folder = folder.resolve()
out_dir.mkdir(parents=True, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archive_base = out_dir / f"{folder.name}_{stamp}"
# creates archive_base.zip
archive_path = shutil.make_archive(str(archive_base), "zip", root_dir=str(folder))
return Path(archive_path)
p = backup_folder(Path("./demo"), Path("./backups"))
print("created", p)How the script works
shutil.make_archive is the standard-library shortcut for building a zip (or tar) file. It takes the base name of the archive without the extension, the format, and the folder to pack. It returns the full path of the file it created, which is why the function returns a Path.
A few decisions in the script matter:
folder.resolve()turns./demointo an absolute path, sofolder.nameis a real name even if you pass..- The timestamp (
20260929_143005) makes every archive name unique and sorts in date order. You never overwrite yesterday's backup by accident. out_dir.mkdir(parents=True, exist_ok=True)creates the backup folder on the first run and does nothing on later runs. That makes the script safe to run twice.root_dirmakes the paths inside the zip relative to the folder, so the archive does not contain your whole drive layout.
Keeping only the newest backups
A backup script that never cleans up will eventually fill the disk. This function keeps the newest keep archives and deletes the rest. Try it with dry_run=True first.
from pathlib import Path
def prune_backups(out_dir: Path, prefix: str, keep: int = 5, dry_run: bool = True):
archives = sorted(out_dir.glob(f"{prefix}_*.zip")) # timestamp names sort oldest first
old = archives[:-keep] if keep > 0 else archives
for path in old:
if dry_run:
print("would delete", path)
else:
path.unlink()
print("deleted", path)
prune_backups(Path("./backups"), "demo", keep=5)Sorting by name works only because the timestamp is in year-month-day order. If you change the format to %d-%m-%Y, the sort breaks.
Common mistakes
- Backing up into the folder you are archiving. The archive then contains the previous archives and grows each run. Keep
out_diroutsidefolder. - Never testing a restore. A backup you have not opened is only a hope. Unzip one now and then and check the files.
- Keeping backups on the same disk. If the disk fails, both copies go. Copy the archive to another drive or a cloud folder as well.
- Using the local clock without thinking.
datetime.now()is local time. That is fine for names, but two runs during a daylight-saving change can sort oddly.
Adapting it
Change "zip" to "gztar" for a smaller .tar.gz. Add a list of folders and loop over it. Schedule the script with cron or Task Scheduler (Phase 6) so it runs every night without you.
Tips
- keep multiple backups
- test restore occasionally
- donβt back up into the same folder youβre archiving
Visualize it
A backup script moves files from the source folder to a safe destination in a few clear steps.
flowchart LR
A["Scan source folder"] --> B["Select files to back up"]
B --> C["Compress into zip archive"]
C --> D["Copy to backup destination"]
π§ͺ Try It Yourself
Exercise 1 β List Files with os.listdir
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.Exercise 2 β Join Paths with os.path.join
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.Exercise 3 β Write and Read a File
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.