Python’s filecmp module compares files and directory trees with shallow (metadata) or deep (byte-by-byte) comparison. import filecmp. File comparison: filecmp.cmp(f1, f2, shallow=True) → bool — shallow=True uses OS signature (size + mtime) only; shallow=False reads file contents. Batch comparison: filecmp.cmpfiles(dir1, dir2, common_names, shallow=True) → (match, mismatch, errors) lists. Directory comparison: dc = filecmp.dircmp(dir1, dir2, ignore=None, hide=None). dircmp attributes: .left_only — names only in dir1; .right_only — names only in dir2; .common — names in both; .same_files — identical files; .diff_files — files with different content; .funny_files — files that could not be compared; .common_dirs — subdirectory names in both; .subdirs — dict of dircmp for each common subdir. Methods: .report() — prints summary to stdout; .report_partial_closure() — prints self + immediate subdirs; .report_full_closure() — full recursive report. Cache: filecmp.clear_cache() — clears internal comparison cache. DEFAULT_IGNORES — default hidden files list. Claude Code generates directory synchronizers, backup validators, diff reporters, and file deduplication tools.
CLAUDE.md for filecmp
## filecmp Stack
- Stdlib: import filecmp
- File: filecmp.cmp(f1, f2) # True if identical (shallow)
- filecmp.cmp(f1, f2, shallow=False) # byte-by-byte comparison
- Batch: match, diff, err = filecmp.cmpfiles(d1, d2, names)
- Dir: dc = filecmp.dircmp(d1, d2)
- dc.same_files dc.diff_files dc.left_only dc.right_only
- dc.subdirs # {name: dircmp, ...} for common subdirs
filecmp File and Directory Comparison Pipeline
# app/filecmputil.py — file compare, dir diff, sync plan, hash compare
from __future__ import annotations
import filecmp
import hashlib
import os
import shutil
from dataclasses import dataclass, field
from pathlib import Path
# ─────────────────────────────────────────────────────────────────────────────
# 1. File comparison helpers
# ─────────────────────────────────────────────────────────────────────────────
def files_identical(f1: str | Path, f2: str | Path, shallow: bool = False) -> bool:
"""
Return True if two files have identical content.
shallow=False forces a byte-by-byte comparison (ignores cached result).
Example:
if files_identical("a.txt", "b.txt"):
print("same content")
"""
filecmp.clear_cache()
return filecmp.cmp(str(f1), str(f2), shallow=shallow)
def file_hash(path: str | Path, algorithm: str = "sha256", chunk: int = 65536) -> str:
"""
Compute a hex digest for a file.
Example:
h1 = file_hash("a.txt")
h2 = file_hash("b.txt")
if h1 == h2: print("same content")
"""
h = hashlib.new(algorithm)
with open(str(path), "rb") as f:
while True:
data = f.read(chunk)
if not data:
break
h.update(data)
return h.hexdigest()
def compare_batch(
dir1: str | Path,
dir2: str | Path,
names: list[str],
shallow: bool = False,
) -> tuple[list[str], list[str], list[str]]:
"""
Compare a list of filenames in two directories.
Returns (same, different, errors).
Example:
names = ["a.txt", "b.txt", "c.txt"]
same, diff, errs = compare_batch("/src", "/dst", names)
"""
return filecmp.cmpfiles(str(dir1), str(dir2), names, shallow=shallow)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Directory difference
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class DiffEntry:
"""A single difference between two directory trees."""
kind: str # "left_only", "right_only", "diff_files", "funny"
path: str # relative path from the root of comparison
left_abs: str | None
right_abs: str | None
def __str__(self) -> str:
if self.kind == "left_only":
return f"< {self.path}"
elif self.kind == "right_only":
return f"> {self.path}"
elif self.kind == "diff_files":
return f"≠ {self.path}"
else:
return f"? {self.path}"
def dir_diff_recursive(
dir1: str | Path,
dir2: str | Path,
ignore: list[str] | None = None,
) -> list[DiffEntry]:
"""
Recursively compare two directory trees, returning all differences as DiffEntry objects.
Example:
diffs = dir_diff_recursive("/src", "/dst")
for d in diffs:
print(d)
"""
dc = filecmp.dircmp(str(dir1), str(dir2), ignore=ignore or [])
results: list[DiffEntry] = []
_collect_diffs(dc, "", str(dir1), str(dir2), results)
return results
def _collect_diffs(
dc: filecmp.dircmp,
prefix: str,
left_root: str,
right_root: str,
results: list[DiffEntry],
) -> None:
for name in dc.left_only:
rel = os.path.join(prefix, name)
results.append(DiffEntry("left_only", rel,
os.path.join(left_root, rel), None))
for name in dc.right_only:
rel = os.path.join(prefix, name)
results.append(DiffEntry("right_only", rel,
None, os.path.join(right_root, rel)))
for name in dc.diff_files:
rel = os.path.join(prefix, name)
results.append(DiffEntry("diff_files", rel,
os.path.join(dc.left, name),
os.path.join(dc.right, name)))
for name in dc.funny_files:
rel = os.path.join(prefix, name)
results.append(DiffEntry("funny", rel,
os.path.join(dc.left, name),
os.path.join(dc.right, name)))
for name, sub_dc in dc.subdirs.items():
_collect_diffs(sub_dc, os.path.join(prefix, name),
left_root, right_root, results)
def same_files_recursive(dir1: str | Path, dir2: str | Path) -> list[str]:
"""
Return list of relative paths that are identical in both trees.
Example:
same = same_files_recursive("/backup", "/original")
print(f"{len(same)} files are identical")
"""
dc = filecmp.dircmp(str(dir1), str(dir2))
result: list[str] = []
_collect_same(dc, "", result)
return result
def _collect_same(dc: filecmp.dircmp, prefix: str, result: list[str]) -> None:
for name in dc.same_files:
result.append(os.path.join(prefix, name))
for name, sub_dc in dc.subdirs.items():
_collect_same(sub_dc, os.path.join(prefix, name), result)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Sync plan generator
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class SyncAction:
action: str # "copy", "delete", "overwrite", "skip"
src: str | None
dst: str
reason: str
def __str__(self) -> str:
return f"[{self.action:9s}] {self.dst} ({self.reason})"
def make_sync_plan(
src_dir: str | Path,
dst_dir: str | Path,
delete_extra: bool = False,
ignore: list[str] | None = None,
) -> list[SyncAction]:
"""
Generate a list of SyncActions needed to make dst_dir match src_dir.
Does NOT execute any changes — call apply_sync_plan() to apply.
Example:
plan = make_sync_plan("/source", "/backup", delete_extra=True)
for action in plan:
print(action)
"""
diffs = dir_diff_recursive(src_dir, dst_dir, ignore=ignore)
actions: list[SyncAction] = []
for diff in diffs:
if diff.kind == "left_only":
# File exists in src but not dst — copy it
src_abs = os.path.join(str(src_dir), diff.path)
dst_abs = os.path.join(str(dst_dir), diff.path)
actions.append(SyncAction("copy", src_abs, dst_abs, "new in source"))
elif diff.kind == "right_only":
# File exists in dst but not src
dst_abs = os.path.join(str(dst_dir), diff.path)
if delete_extra:
actions.append(SyncAction("delete", None, dst_abs, "removed from source"))
else:
actions.append(SyncAction("skip", None, dst_abs, "extra in dest (kept)"))
elif diff.kind == "diff_files":
src_abs = os.path.join(str(src_dir), diff.path)
dst_abs = os.path.join(str(dst_dir), diff.path)
actions.append(SyncAction("overwrite", src_abs, dst_abs, "content changed"))
return actions
def apply_sync_plan(plan: list[SyncAction], dry_run: bool = False) -> list[SyncAction]:
"""
Execute a list of SyncActions produced by make_sync_plan().
Returns the list of actions that were applied (or would be, if dry_run=True).
Example:
plan = make_sync_plan("/source", "/backup")
applied = apply_sync_plan(plan, dry_run=True) # preview
apply_sync_plan(plan) # actually do it
"""
applied = []
for action in plan:
if action.action == "copy":
if not dry_run:
dst_path = Path(action.dst)
dst_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(action.src, action.dst)
applied.append(action)
elif action.action == "overwrite":
if not dry_run:
shutil.copy2(action.src, action.dst)
applied.append(action)
elif action.action == "delete":
if not dry_run:
p = Path(action.dst)
if p.is_dir():
shutil.rmtree(action.dst)
else:
p.unlink(missing_ok=True)
applied.append(action)
return applied
# ─────────────────────────────────────────────────────────────────────────────
# 4. Duplicate file finder
# ─────────────────────────────────────────────────────────────────────────────
def find_duplicate_files(
directories: list[str | Path],
algorithm: str = "sha256",
) -> dict[str, list[Path]]:
"""
Find files with identical content across (and within) the given directories.
Returns {hash: [path, path, ...]} for all hash values with >1 file.
Example:
dupes = find_duplicate_files(["/photos", "/backup/photos"])
for h, paths in dupes.items():
print(f" {h[:12]}... × {len(paths)}")
for p in paths:
print(f" {p}")
"""
hash_to_paths: dict[str, list[Path]] = {}
for d in directories:
for p in Path(d).rglob("*"):
if p.is_file():
h = file_hash(p, algorithm=algorithm)
hash_to_paths.setdefault(h, []).append(p)
return {h: paths for h, paths in hash_to_paths.items() if len(paths) > 1}
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile
print("=== filecmp demo ===")
with tempfile.TemporaryDirectory() as tmp:
# Create source tree
src = Path(tmp) / "src"
dst = Path(tmp) / "dst"
for d in [src, dst]:
d.mkdir()
(d / "sub").mkdir()
# same in both
(src / "common.txt").write_text("hello world")
(dst / "common.txt").write_text("hello world")
# different
(src / "changed.txt").write_text("version 2")
(dst / "changed.txt").write_text("version 1")
# left only
(src / "new.txt").write_text("only in source")
# right only
(dst / "old.txt").write_text("only in dest")
# subdirs
(src / "sub" / "deep.txt").write_text("deep file")
(dst / "sub" / "deep.txt").write_text("different deep")
# ── files_identical ──────────────────────────────────────────────────
print("\n--- files_identical ---")
print(f" common.txt: {files_identical(src/'common.txt', dst/'common.txt')}")
print(f" changed.txt: {files_identical(src/'changed.txt', dst/'changed.txt', shallow=False)}")
# ── dir_diff_recursive ───────────────────────────────────────────────
print("\n--- dir_diff_recursive ---")
for d in dir_diff_recursive(src, dst):
print(f" {d}")
# ── same_files_recursive ─────────────────────────────────────────────
print("\n--- same_files_recursive ---")
print(f" same: {same_files_recursive(src, dst)}")
# ── make_sync_plan ───────────────────────────────────────────────────
print("\n--- make_sync_plan (no delete) ---")
plan = make_sync_plan(src, dst, delete_extra=False)
for a in plan:
print(f" {a}")
print("\n--- apply_sync_plan (dry_run=True) ---")
applied = apply_sync_plan(plan, dry_run=True)
print(f" would apply {len(applied)} action(s)")
print("\n--- apply_sync_plan (real) ---")
apply_sync_plan(plan)
# Verify
diffs_after = dir_diff_recursive(src, dst)
non_skip = [d for d in diffs_after if d.kind != "right_only"]
print(f" differences after sync: {non_skip}")
# ── find_duplicate_files ─────────────────────────────────────────────
print("\n--- find_duplicate_files ---")
# Plant a duplicate
(src / "dup1.txt").write_text("duplicate content")
(src / "sub" / "dup2.txt").write_text("duplicate content")
dupes = find_duplicate_files([src])
for h, paths in dupes.items():
print(f" {h[:16]}... × {len(paths)}")
for p in paths:
print(f" {p.relative_to(tmp)}")
print("\n=== done ===")
For the difflib alternative — difflib.ndiff(lines1, lines2), difflib.unified_diff(lines1, lines2), and difflib.HtmlDiff().make_file(lines1, lines2) produce textual diffs of file content — use difflib when you need to understand what changed in a file (character-level or line-level differences); use filecmp when you need to know whether files match or which files in a directory tree differ, without reading all their content (the shallow OS-signature comparison is much faster for backup validation). For the shutil alternative — shutil.copytree(src, dst, copy_function=shutil.copy2) and shutil.copytree(src, dst, dirs_exist_ok=True) copy directory trees — use shutil.copytree for a one-shot copy; use filecmp.dircmp + make_sync_plan() / apply_sync_plan() when you need to sync incrementally (only copy what changed, optionally delete extras) and want to preview the changes before committing. The Claude Skills 360 bundle includes filecmp skill sets covering files_identical()/file_hash()/compare_batch() file comparisons, DiffEntry with dir_diff_recursive()/same_files_recursive() tree diffing, SyncAction with make_sync_plan()/apply_sync_plan() incremental sync, and find_duplicate_files() content-based deduplication. Start with the free tier to try directory comparison patterns and filecmp pipeline code generation.