Python’s shutil module provides high-level file and directory operations. import shutil. copy: shutil.copy(src, dst) — copies file, no metadata. copy2: shutil.copy2(src, dst) — preserves timestamps. copyfileobj: shutil.copyfileobj(fsrc, fdst, length=16384). copytree: shutil.copytree(src, dst) — recursive copy; dirs_exist_ok=True (3.8+). move: shutil.move(src, dst) — rename or cross-device copy+delete. rmtree: shutil.rmtree(path) — recursive delete; ignore_errors=True to swallow errors. disk_usage: shutil.disk_usage(path) → named tuple (total, used, free) in bytes. which: shutil.which("git") → path or None. get_terminal_size: shutil.get_terminal_size((80, 24)). make_archive: shutil.make_archive("backup", "zip", root_dir="/data") — returns archive path. unpack_archive: shutil.unpack_archive("backup.zip", extract_dir="/out"). get_archive_formats: shutil.get_archive_formats(). ignore_patterns: shutil.copytree(src, dst, ignore=shutil.ignore_patterns("*.pyc", "__pycache__")). chown: shutil.chown(path, user="www", group="www"). SameFileError: raised by copy/copy2 when src == dst. Error: base shutil exception. copystat: shutil.copystat(src, dst) — copy permissions, timestamps, flags. Claude Code generates backup utilities, deployment copy scripts, archive pipelines, and disk-usage reporters.
CLAUDE.md for shutil
## shutil Stack
- Stdlib: import shutil, pathlib.Path, tempfile
- Copy file: shutil.copy2(src, dst) — preserves metadata
- Copy tree: shutil.copytree(src, dst, ignore=shutil.ignore_patterns("*.pyc"))
- Move: shutil.move(str(src), str(dst)) — works cross-device
- Delete tree: shutil.rmtree(path, ignore_errors=True)
- Archive: shutil.make_archive(base_name, "zip", root_dir=src)
- Disk: shutil.disk_usage(path).free — bytes free
shutil File Management Pipeline
# app/fsutil.py — copy, move, rmtree, archive, disk_usage, atomic write
from __future__ import annotations
import hashlib
import os
import shutil
import tempfile
from datetime import datetime
from pathlib import Path
from typing import Callable, Iterator
# ─────────────────────────────────────────────────────────────────────────────
# 1. Copy helpers
# ─────────────────────────────────────────────────────────────────────────────
def copy_file(
src: str | Path,
dst: str | Path,
preserve_metadata: bool = True,
overwrite: bool = True,
) -> Path:
"""
Copy a single file; return destination Path.
Example:
copy_file("config.yaml", "/backup/config.yaml")
copy_file(src, dst, overwrite=False) # skip if dst exists
"""
src, dst = Path(src), Path(dst)
if not overwrite and dst.exists():
return dst
dst.parent.mkdir(parents=True, exist_ok=True)
fn = shutil.copy2 if preserve_metadata else shutil.copy
fn(src, dst)
return dst
def copy_tree(
src: str | Path,
dst: str | Path,
ignore: tuple[str, ...] = ("*.pyc", "__pycache__", ".git", "*.egg-info"),
overwrite: bool = True,
) -> Path:
"""
Recursively copy a directory tree.
Example:
copy_tree("src", "dist/src", ignore=("*.pyc", "*.log"))
"""
src, dst = Path(src), Path(dst)
if dst.exists() and overwrite:
shutil.rmtree(dst)
shutil.copytree(
src,
dst,
ignore=shutil.ignore_patterns(*ignore) if ignore else None,
dirs_exist_ok=not overwrite,
)
return dst
def mirror_tree(
src: str | Path,
dst: str | Path,
ignore: tuple[str, ...] = ("*.pyc", "__pycache__"),
) -> tuple[int, int]:
"""
One-way sync: copy new/changed files from src to dst, return (copied, deleted).
Files removed from src are NOT deleted from dst (use copy_tree for a full mirror).
Example:
copied, deleted = mirror_tree("app", "dist/app")
"""
src, dst = Path(src), Path(dst)
copied = 0
for s_path in src.rglob("*"):
rel = s_path.relative_to(src)
d_path = dst / rel
if s_path.is_dir():
d_path.mkdir(parents=True, exist_ok=True)
continue
if any(s_path.match(pat) for pat in ignore):
continue
if not d_path.exists() or s_path.stat().st_mtime > d_path.stat().st_mtime:
d_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(s_path, d_path)
copied += 1
return copied, 0
# ─────────────────────────────────────────────────────────────────────────────
# 2. Move and delete
# ─────────────────────────────────────────────────────────────────────────────
def move_path(src: str | Path, dst: str | Path) -> Path:
"""
Move file or directory; create destination parent dirs automatically.
Example:
move_path("tmp/output.csv", "results/2024/output.csv")
"""
src, dst = Path(src), Path(dst)
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dst))
return dst
def remove_tree(path: str | Path, missing_ok: bool = True) -> bool:
"""
Recursively remove a directory tree. Returns True if something was removed.
Example:
remove_tree("dist")
remove_tree(".cache", missing_ok=True)
"""
p = Path(path)
if not p.exists():
return False
if p.is_file() or p.is_symlink():
p.unlink()
else:
shutil.rmtree(p)
return True
def clean_dir(path: str | Path, pattern: str = "*") -> int:
"""
Delete all files matching pattern inside path (non-recursive). Returns count.
Example:
clean_dir("logs", "*.log")
clean_dir("dist")
"""
p = Path(path)
count = 0
for f in p.glob(pattern):
if f.is_file():
f.unlink()
count += 1
return count
# ─────────────────────────────────────────────────────────────────────────────
# 3. Archive helpers
# ─────────────────────────────────────────────────────────────────────────────
def create_archive(
source: str | Path,
output: str | Path | None = None,
fmt: str = "zip",
) -> Path:
"""
Create an archive of a directory or file. Returns path to archive.
Example:
path = create_archive("dist", "releases/v1.0")
path = create_archive("data", fmt="gztar") # .tar.gz
"""
source = Path(source)
if output is None:
output = source.parent / source.name
output = Path(str(output))
archive = shutil.make_archive(
base_name=str(output),
format=fmt,
root_dir=str(source.parent),
base_dir=source.name,
)
return Path(archive)
def extract_archive(
archive: str | Path,
dest: str | Path | None = None,
) -> Path:
"""
Extract an archive (zip, tar, gztar, etc.). Returns extraction directory.
Example:
dest = extract_archive("release.zip", "/tmp/release")
dest = extract_archive("backup.tar.gz")
"""
archive = Path(archive)
if dest is None:
dest = archive.parent / archive.stem.split(".")[0]
dest = Path(dest)
dest.mkdir(parents=True, exist_ok=True)
shutil.unpack_archive(str(archive), str(dest))
return dest
def backup_directory(
source: str | Path,
backup_dir: str | Path,
fmt: str = "zip",
timestamp: bool = True,
) -> Path:
"""
Create a timestamped archive backup. Returns archive path.
Example:
path = backup_directory("data", "/backups")
# → /backups/data_20240115_103000.zip
"""
source = Path(source)
backup_dir = Path(backup_dir)
backup_dir.mkdir(parents=True, exist_ok=True)
suffix = f"_{datetime.now().strftime('%Y%m%d_%H%M%S')}" if timestamp else ""
base = backup_dir / f"{source.name}{suffix}"
return create_archive(source, base, fmt=fmt)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Disk usage and file information
# ─────────────────────────────────────────────────────────────────────────────
def disk_free(path: str | Path = ".") -> int:
"""
Return free bytes on the filesystem containing path.
Example:
free = disk_free("/data")
if free < 1_000_000_000:
alert("Less than 1 GB free")
"""
return shutil.disk_usage(str(path)).free
def disk_stats(path: str | Path = ".") -> dict[str, int]:
"""
Return dict with total, used, free bytes and used_pct.
Example:
stats = disk_stats("/")
print(f"Disk {stats['used_pct']:.1f}% full")
"""
usage = shutil.disk_usage(str(path))
return {
"total": usage.total,
"used": usage.used,
"free": usage.free,
"used_pct": round(usage.used / usage.total * 100, 2),
}
def dir_size(path: str | Path) -> int:
"""
Calculate total size in bytes of all files under path.
Example:
size = dir_size("dist")
print(f"dist: {size / 1_048_576:.1f} MB")
"""
return sum(
f.stat().st_size
for f in Path(path).rglob("*")
if f.is_file()
)
def find_executable(name: str) -> str | None:
"""
Return full path to executable on PATH or None.
Example:
git = find_executable("git")
if not git:
raise EnvironmentError("git not found")
"""
return shutil.which(name)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Atomic write and safe temp operations
# ─────────────────────────────────────────────────────────────────────────────
def atomic_write_bytes(path: str | Path, data: bytes) -> Path:
"""
Write data atomically by writing to a temp file then renaming.
Prevents partial writes being visible.
Example:
atomic_write_bytes("config.json", json.dumps(cfg).encode())
"""
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=p.parent, prefix=".tmp_")
try:
with os.fdopen(fd, "wb") as f:
f.write(data)
shutil.move(tmp, str(p))
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
raise
return p
def atomic_write_text(path: str | Path, text: str, encoding: str = "utf-8") -> Path:
"""
Write text atomically.
Example:
atomic_write_text("README.md", content)
"""
return atomic_write_bytes(path, text.encode(encoding))
def safe_copy_with_checksum(
src: str | Path,
dst: str | Path,
verify: bool = True,
) -> str:
"""
Copy file and verify SHA-256 checksum on both ends.
Returns hex digest. Raises ValueError if checksums differ.
Example:
digest = safe_copy_with_checksum("release.zip", "/backup/release.zip")
"""
src, dst = Path(src), Path(dst)
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
if verify:
def sha256(p: Path) -> str:
h = hashlib.sha256()
with open(p, "rb") as f:
while chunk := f.read(65536):
h.update(chunk)
return h.hexdigest()
src_hash = sha256(src)
dst_hash = sha256(dst)
if src_hash != dst_hash:
dst.unlink(missing_ok=True)
raise ValueError(f"Checksum mismatch: {src_hash} != {dst_hash}")
return src_hash
return ""
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile as _tmp
print("=== shutil demo ===")
with _tmp.TemporaryDirectory() as td:
base = Path(td)
# Setup source tree
src = base / "project"
(src / "app").mkdir(parents=True)
(src / "app" / "main.py").write_text("print('hello')")
(src / "app" / "utils.py").write_text("# utils")
(src / "app" / "__pycache__").mkdir()
(src / "app" / "__pycache__" / "main.cpython-312.pyc").write_bytes(b"\x00pycache")
(src / "README.md").write_text("# Project")
print("\n--- copy_file ---")
dst_file = copy_file(src / "README.md", base / "backup" / "README.md")
print(f" copied: {dst_file.exists()}, path: {dst_file.relative_to(base)}")
print("\n--- copy_tree (ignore __pycache__) ---")
dst_tree = copy_tree(src, base / "dist_tree")
pycache = base / "dist_tree" / "app" / "__pycache__"
print(f" tree exists: {dst_tree.exists()}")
print(f" __pycache__ ignored: {not pycache.exists()}")
print(f" main.py copied: {(dst_tree / 'app' / 'main.py').exists()}")
print("\n--- move_path ---")
(base / "staging").mkdir()
(base / "staging" / "data.csv").write_text("a,b\n1,2")
moved = move_path(base / "staging" / "data.csv", base / "results" / "final.csv")
print(f" moved to: {moved.relative_to(base)}")
print("\n--- create_archive ---")
archive = create_archive(src, base / "releases" / "project_v1")
print(f" archive: {archive.name}, size: {archive.stat().st_size} bytes")
print("\n--- extract_archive ---")
out = extract_archive(archive, base / "extracted")
print(f" extracted: {[p.name for p in out.rglob('*.py')]}")
print("\n--- backup_directory ---")
bak = backup_directory(src, base / "backups", timestamp=False)
print(f" backup: {bak.name}")
print("\n--- disk_stats ---")
stats = disk_stats(base)
print(f" total: {stats['total']//1_048_576} MB, "
f"used: {stats['used_pct']}%")
print("\n--- dir_size ---")
sz = dir_size(src)
print(f" project size: {sz} bytes")
print("\n--- find_executable ---")
for exe in ["python3", "git", "nonexistent_xyz"]:
path = find_executable(exe)
print(f" {exe}: {path or 'NOT FOUND'}")
print("\n--- atomic_write_text ---")
cfg = base / "config.json"
atomic_write_text(cfg, '{"debug": true}')
print(f" written: {cfg.read_text()!r}")
print("\n--- safe_copy_with_checksum ---")
digest = safe_copy_with_checksum(cfg, base / "config.backup.json")
print(f" digest: {digest[:16]}..., verified OK")
print("\n--- clean_dir ---")
logs = base / "logs"
logs.mkdir()
for i in range(3):
(logs / f"app.{i}.log").write_text(f"log {i}")
removed = clean_dir(logs, "*.log")
print(f" removed {removed} log files")
print("\n=== done ===")
For the pathlib alternative — pathlib.Path provides object-oriented file-path manipulation with .read_bytes(), .write_bytes(), .mkdir(), .unlink(), .rename(), and .glob(), but lacks tree-copy, archiving, and disk-usage; shutil fills that gap — use pathlib for individual file I/O and path construction, shutil for bulk operations (copy trees, archives, cross-device moves) where higher-level orchestration is needed. For the send2trash alternative — send2trash (PyPI) moves files to the OS recycle bin instead of permanently deleting them, providing a safety net for interactive tools; shutil.rmtree permanently deletes with no recovery — use send2trash for desktop applications, interactive CLIs, or tooling where accidental deletion of user data would be catastrophic, shutil.rmtree for build artifacts, CI pipelines, and ephemeral directories where permanent deletion is correct. The Claude Skills 360 bundle includes shutil skill sets covering copy_file()/copy_tree()/mirror_tree() copy helpers, move_path()/remove_tree()/clean_dir() move and delete utilities, create_archive()/extract_archive()/backup_directory() archive pipeline, disk_free()/disk_stats()/dir_size()/find_executable() introspection, and atomic_write_bytes()/safe_copy_with_checksum() safe-write utilities. Start with the free tier to try file management and shutil pipeline code generation.