Create a CSV list of files in a folder
Scan subfolders and record file paths, extensions, sizes, and modification times in a table. Start with 4 small example files while keeping the originals and existing results intact.
Create a dated ZIP archive from a folder without changing the originals, then verify that every source file appears in the archive with matching content. A small synthetic folder makes the expected paths, sizes, and totals easy to check by hand.
Who this is forThis guide is for people who want a simple dated folder backup with an explicit verification step instead of assuming that archive creation succeeded.
The script will archive regular files from one source folder, including files in nested subfolders. Paths inside the ZIP are stored relative to the source folder, so a file such as reports/week1.txt stays reports/week1.txt instead of becoming an absolute filesystem path.
The archive name contains the current local calendar date in YYYY-MM-DD form. If the example is run on 2026-09-20, the expected filename is backup_demo_2026-09-20.zip. Running it again on the same day with the same destination causes the script to stop instead of replacing the previous backup.
The following dataset is synthetic and was written specifically for this article. Save the setup script as create_backup_demo.py. It creates four files with exact byte contents, including one file in a nested reports folder.
from pathlib import Path
SOURCE = Path("outputs") / "backup_demo"
FILES = {
"notes.txt": b"alpha\n",
"reports/week1.txt": b"beta\n",
"reports/week2.txt": b"gamma\n",
"settings.ini": b"mode=test\n",
}
SOURCE.parent.mkdir(parents=True, exist_ok=True)
SOURCE.mkdir() # Stop if the synthetic source already exists.
for relative_name, content in FILES.items():
target = SOURCE / relative_name
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("xb") as stream:
stream.write(content)
python create_backup_demo.py| Relative path | Exact content | Bytes |
|---|---|---|
| notes.txt | alpha plus newline | 6 |
| reports/week1.txt | beta plus newline | 5 |
| reports/week2.txt | gamma plus newline | 6 |
| settings.ini | mode=test plus newline | 10 |
The source contains 4 regular files totaling 27 bytes of uncompressed content: 6 + 5 + 6 + 10. The size of the final ZIP file itself cannot be predicted from that total because ZIP compression and archive metadata add their own overhead.
python dated_zip_backup.pyThe script checks that the source exists and that the backup directory is not inside the source directory. The backup parent folder may already exist. The dated ZIP file itself must not exist.
The script first builds a sorted list of source files. It creates the archive with exclusive file creation, writes each file with ZIP_DEFLATED compression, and then reopens the archive for verification. Verification compares the intended member list, checks CRC integrity with testzip, and compares source and archived SHA-256 hashes.
from datetime import date
import hashlib
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile
SOURCE = Path("outputs") / "backup_demo"
BACKUP_DIR = Path("outputs") / "backups"
CHUNK_SIZE = 1024 * 1024
def hash_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(CHUNK_SIZE):
digest.update(chunk)
return digest.hexdigest()
def hash_member(archive: ZipFile, member_name: str) -> str:
digest = hashlib.sha256()
with archive.open(member_name, "r") as stream:
while chunk := stream.read(CHUNK_SIZE):
digest.update(chunk)
return digest.hexdigest()
def main() -> None:
source_root = SOURCE.resolve(strict=True)
if not source_root.is_dir():
raise ValueError("SOURCE must be a directory.")
backup_root = BACKUP_DIR.resolve()
if backup_root.is_relative_to(source_root):
raise ValueError("BACKUP_DIR must be outside SOURCE.")
files = sorted(
path for path in source_root.rglob("*")
if path.is_file() and not path.is_symlink()
)
if not files:
raise ValueError("SOURCE contains no regular files to back up.")
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
archive_name = f"{source_root.name}_{date.today().isoformat()}.zip"
archive_path = BACKUP_DIR / archive_name
if archive_path.exists():
raise FileExistsError(f"Backup already exists: {archive_path}")
expected = {}
with archive_path.open("xb") as raw:
with ZipFile(raw, "w", compression=ZIP_DEFLATED) as archive:
for path in files:
relative = path.relative_to(source_root).as_posix()
expected[relative] = (path.stat().st_size, hash_file(path))
archive.write(path, arcname=relative)
with ZipFile(archive_path, "r") as archive:
bad_member = archive.testzip()
if bad_member is not None:
raise RuntimeError(f"CRC check failed: {bad_member}")
members = [info for info in archive.infolist() if not info.is_dir()]
actual_names = [info.filename for info in members]
expected_names = list(expected)
if actual_names != expected_names:
raise RuntimeError("Archive member list does not match the source list.")
for info in members:
expected_size, expected_hash = expected[info.filename]
if info.file_size != expected_size:
raise RuntimeError(f"Size mismatch: {info.filename}")
if hash_member(archive, info.filename) != expected_hash:
raise RuntimeError(f"Hash mismatch: {info.filename}")
total_bytes = sum(size for size, _ in expected.values())
print(f"Verified {len(expected)} files totaling {total_bytes} bytes.")
print(f"Archive: {archive_path.as_posix()}")
if __name__ == "__main__":
main()
The code skips symbolic links and archives regular files only. The expected hashes are calculated from the source immediately before each file is added. For a normal local folder that remains unchanged during the run, this provides a useful content-level comparison after the archive is created.
If the script is run on 2026-09-20, the expected archive path is outputs/backups/backup_demo_2026-09-20.zip. The ZIP should contain exactly the following four member paths.
| Archive member | Uncompressed bytes |
|---|---|
| notes.txt | 6 |
| reports/week1.txt | 5 |
| reports/week2.txt | 6 |
| settings.ini | 10 |
The nested reports folder is represented by the member paths themselves. The script does not need a separate directory entry for reports because the files already carry that relative path.
The expected console output below is derived manually for a run on 2026-09-20. It is not a captured execution log.
Verified 4 files totaling 27 bytes.
Archive: outputs/backups/backup_demo_2026-09-20.zipDo not use the archive file size as the primary verification check. Compression means a valid archive can be smaller or larger than the uncompressed source total, depending on the data and metadata.
| Symptom | What to check |
|---|---|
| FileNotFoundError | Check SOURCE and the terminal's working directory. Run the synthetic setup script first for this example. |
| FileExistsError | A backup with the same dated filename already exists. Review it and choose a different destination or wait for a different intended backup date instead of overwriting it. |
| PermissionError | Confirm that the script can read every source file and create files inside BACKUP_DIR. |
| CRC check failed | Treat the archive as failed. Do not rely on it as a backup; investigate storage or write errors and create a fresh archive. |
| Member list mismatch | The source may have changed during the run, or the archive contents may not match the file list assembled at the beginning. |
| Hash or size mismatch | Treat verification as failed. Keep the original source unchanged and create a new archive after investigating the mismatch. |
If an exception occurs after the ZIP file has been created, a partial or unverified archive can remain in the backup folder. The script does not automatically delete it because automatic cleanup can hide evidence useful for diagnosing the failure.
This is a file-copy archive workflow, not a transactional filesystem snapshot. If another application modifies a file while it is being hashed or archived, the result can become inconsistent. For important data, stop writes to the source or use a snapshot-capable backup system.
The example does not preserve every filesystem property. Permissions, ownership, access-control lists, extended attributes, alternate data streams, symbolic links, and application-specific metadata may require a different backup tool. ZIP is useful for portable file collections, but it is not a complete image of a filesystem.
A verified archive is still only one copy. A real backup plan should consider multiple copies, storage failure, accidental deletion, ransomware, retention periods, encryption, and whether recovery has actually been tested. Verification here shows that this archive matched the source content observed during this run; it does not establish a complete disaster-recovery strategy.
2026-09-21 · ChatGPT Python tool · Python 3.13.5 · standard library datetime/hashlib/pathlib/zipfile
The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.