shutil handles file operations that os doesn't—copying, moving, and archiving. Here's how to use it.
Copying Files
import shutil
# Copy file (preserves permissions)
shutil.copy("source.txt", "dest.txt")
shutil.copy("source.txt", "dest_dir/") # Into directory
# Copy with metadata (timestamps, permissions)
shutil.copy2("source.txt", "dest.txt")
# Copy file object
with open("source.txt", "rb") as src:
with open("dest.txt", "wb") as dst:
shutil.copyfileobj(src, dst)
# Copy just the content (no metadata)
shutil.copyfile("source.txt", "dest.txt")Copying Directories
import shutil
# Copy entire directory tree
shutil.copytree("src_dir", "dst_dir")
# With ignore patterns
shutil.copytree(
"src_dir",
"dst_dir",
ignore=shutil.ignore_patterns("*.pyc", "__pycache__")
)
# Custom ignore function
def ignore_hidden(dir, files):
return [f for f in files if f.startswith(".")]
shutil.copytree("src_dir", "dst_dir", ignore=ignore_hidden)
# Copy into existing directory (Python 3.8+)
shutil.copytree("src_dir", "dst_dir", dirs_exist_ok=True)Moving Files and Directories
import shutil
# Move file or directory
shutil.move("old_path", "new_path")
# Move into directory
shutil.move("file.txt", "dest_dir/")
# Works across filesystems (copy then delete)
shutil.move("/mnt/usb/file.txt", "/home/user/file.txt")Removing Directories
import shutil
# Remove directory tree (careful!)
shutil.rmtree("directory")
# With error handling
def on_error(func, path, exc_info):
print(f"Error removing {path}: {exc_info[1]}")
shutil.rmtree("directory", onerror=on_error)
# Ignore errors
shutil.rmtree("directory", ignore_errors=True)
# Python 3.12+: onexc instead of onerror
# shutil.rmtree("directory", onexc=handler)Disk Usage
import shutil
# Get disk usage statistics
usage = shutil.disk_usage("/")
print(f"Total: {usage.total / 1e9:.1f} GB")
print(f"Used: {usage.used / 1e9:.1f} GB")
print(f"Free: {usage.free / 1e9:.1f} GB")
# As percentage
percent_used = usage.used / usage.total * 100
print(f"Used: {percent_used:.1f}%")Finding Executables
import shutil
# Find executable in PATH
python_path = shutil.which("python")
print(python_path) # /usr/bin/python
# Returns None if not found
missing = shutil.which("nonexistent")
print(missing) # None
# Custom PATH
shutil.which("python", path="/custom/bin:/usr/bin")Archives
import shutil
# Create archive
shutil.make_archive(
"backup", # Archive name (without extension)
"zip", # Format: zip, tar, gztar, bztar, xztar
"source_dir" # Directory to archive
)
# Creates backup.zip
# With base directory
shutil.make_archive(
"backup",
"gztar", # Creates .tar.gz
root_dir="/home",
base_dir="user/documents"
)
# Extract archive
shutil.unpack_archive("backup.zip", "extract_dir")
shutil.unpack_archive("backup.tar.gz", "extract_dir")
# List supported formats
print(shutil.get_archive_formats())
# [('bztar', "bzip2'ed tar-file"), ('gztar', "gzip'ed tar-file"), ...]
print(shutil.get_unpack_formats())Terminal Size
import shutil
# Get terminal dimensions
size = shutil.get_terminal_size()
print(f"Terminal: {size.columns}x{size.lines}")
# With fallback
size = shutil.get_terminal_size(fallback=(80, 24))Copying Metadata
import shutil
import os
# Copy permissions
shutil.copymode("source.txt", "dest.txt")
# Copy all metadata (permissions, timestamps, flags)
shutil.copystat("source.txt", "dest.txt")
# Copy owner and group (Unix, requires privileges)
# shutil.chown("file.txt", user="newowner", group="newgroup")Safe File Operations
import shutil
import tempfile
from pathlib import Path
def safe_copy(src: Path, dst: Path) -> None:
"""Copy file safely using temp file."""
dst = Path(dst)
with tempfile.NamedTemporaryFile(
dir=dst.parent,
delete=False
) as tmp:
tmp_path = Path(tmp.name)
try:
shutil.copy2(src, tmp_path)
tmp_path.replace(dst) # Atomic on POSIX
except Exception:
tmp_path.unlink(missing_ok=True)
raise
def safe_move(src: Path, dst: Path) -> None:
"""Move file with verification."""
src, dst = Path(src), Path(dst)
src_size = src.stat().st_size
shutil.copy2(src, dst)
if dst.stat().st_size == src_size:
src.unlink()
else:
dst.unlink()
raise IOError("Copy verification failed")Backup Patterns
import shutil
from datetime import datetime
from pathlib import Path
def backup_file(path: Path) -> Path:
"""Create timestamped backup."""
path = Path(path)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup = path.with_name(f"{path.stem}_{timestamp}{path.suffix}")
shutil.copy2(path, backup)
return backup
def rotate_backups(pattern: str, keep: int = 5) -> None:
"""Keep only N most recent backups."""
backups = sorted(Path(".").glob(pattern), reverse=True)
for old_backup in backups[keep:]:
old_backup.unlink()Directory Sync
import shutil
from pathlib import Path
def sync_directories(src: Path, dst: Path) -> None:
"""Sync source to destination."""
src, dst = Path(src), Path(dst)
# Copy new and updated files
for src_file in src.rglob("*"):
if src_file.is_file():
rel_path = src_file.relative_to(src)
dst_file = dst / rel_path
if not dst_file.exists():
dst_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_file, dst_file)
elif src_file.stat().st_mtime > dst_file.stat().st_mtime:
shutil.copy2(src_file, dst_file)Common Patterns
import shutil
from pathlib import Path
# Copy if newer
def copy_if_newer(src: Path, dst: Path) -> bool:
src, dst = Path(src), Path(dst)
if not dst.exists() or src.stat().st_mtime > dst.stat().st_mtime:
shutil.copy2(src, dst)
return True
return False
# Ensure directory exists and copy into it
def copy_to_dir(src: Path, dst_dir: Path) -> Path:
dst_dir = Path(dst_dir)
dst_dir.mkdir(parents=True, exist_ok=True)
dst = dst_dir / Path(src).name
shutil.copy2(src, dst)
return dst
# Check space before copy
def safe_copy_check_space(src: Path, dst: Path) -> None:
src, dst = Path(src), Path(dst)
needed = src.stat().st_size
available = shutil.disk_usage(dst.parent).free
if needed > available:
raise IOError(f"Need {needed} bytes, only {available} available")
shutil.copy2(src, dst)shutil is your go-to for file operations beyond basic open/read/write. Use it for copying, moving, archiving, and cleaning up directory trees.
React to this post: