The shutil module provides high-level file operations that go beyond what os offers. While os handles low-level operations, shutil gives you the convenience functions you actually want.
Copying Files
import shutil
# Copy file (preserves permissions)
shutil.copy('source.txt', 'dest.txt')
shutil.copy('source.txt', 'dest_dir/') # copies into directory
# Copy file + metadata (timestamps, permissions)
shutil.copy2('source.txt', 'dest.txt')
# Copy just the file content
shutil.copyfile('source.txt', 'dest.txt')
# Copy file permissions
shutil.copymode('source.txt', 'dest.txt')
# Copy file metadata (timestamps)
shutil.copystat('source.txt', 'dest.txt')The difference: copy() preserves permissions, copy2() also preserves timestamps, copyfile() just copies bytes.
Copying Directories
# Copy entire directory tree
shutil.copytree('src_dir', 'dst_dir')
# Ignore certain patterns
shutil.copytree(
'src_dir',
'dst_dir',
ignore=shutil.ignore_patterns('*.pyc', '__pycache__')
)
# Copy into existing directory (Python 3.8+)
shutil.copytree('src_dir', 'dst_dir', dirs_exist_ok=True)Moving and Renaming
# Move file or directory
shutil.move('source', 'destination')
# Works across filesystems (copy + delete)
shutil.move('/mnt/disk1/file.txt', '/mnt/disk2/file.txt')shutil.move() handles cross-filesystem moves automatically, unlike os.rename().
Deleting Directory Trees
# Remove directory and all contents
shutil.rmtree('directory')
# Ignore errors during deletion
shutil.rmtree('directory', ignore_errors=True)
# Custom error handler
def on_error(func, path, exc_info):
print(f"Error deleting {path}: {exc_info[1]}")
shutil.rmtree('directory', onerror=on_error)Warning: rmtree is destructive and immediate. No recycle bin.
Creating Archives
# Create a zip archive
shutil.make_archive('backup', 'zip', 'source_dir')
# Creates: backup.zip
# Create a tar.gz archive
shutil.make_archive('backup', 'gztar', 'source_dir')
# Creates: backup.tar.gz
# Supported formats: zip, tar, gztar, bztar, xztarExtracting Archives
# Extract archive to directory
shutil.unpack_archive('backup.zip', 'extract_dir')
# Format auto-detected from extension
shutil.unpack_archive('backup.tar.gz', 'extract_dir')Disk Usage
# Get disk usage statistics
usage = shutil.disk_usage('/')
print(f"Total: {usage.total // (1024**3)} GB")
print(f"Used: {usage.used // (1024**3)} GB")
print(f"Free: {usage.free // (1024**3)} GB")Returns a named tuple with total, used, and free bytes.
Finding Executables
# Find executable in PATH
python_path = shutil.which('python')
# Returns: '/usr/bin/python' or None if not found
# Check if command exists
if shutil.which('git'):
print("Git is installed")Terminal Size
# Get terminal dimensions
size = shutil.get_terminal_size()
print(f"Terminal: {size.columns}x{size.lines}")Practical Example: Backup Script
import shutil
from datetime import datetime
from pathlib import Path
def backup_project(project_dir: str, backup_dir: str) -> str:
"""Create timestamped backup of a project."""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
project_name = Path(project_dir).name
backup_name = f"{project_name}_{timestamp}"
# Create archive
archive_path = shutil.make_archive(
str(Path(backup_dir) / backup_name),
'gztar',
project_dir,
logger=None
)
return archive_path
# Usage
backup_file = backup_project('./my_project', './backups')
print(f"Backup created: {backup_file}")Quick Reference
| Function | Purpose |
|---|---|
copy() | Copy file, preserve permissions |
copy2() | Copy file, preserve all metadata |
copytree() | Copy directory recursively |
move() | Move file/directory (cross-filesystem safe) |
rmtree() | Delete directory tree |
make_archive() | Create zip/tar archive |
unpack_archive() | Extract archive |
disk_usage() | Get disk space info |
which() | Find executable in PATH |
shutil handles the tedious parts of file operations so you can focus on the logic.
React to this post: