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, xztar

Extracting 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

FunctionPurpose
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: