shutil
import shutil The shutil module provides high-level file operations that go beyond what the os module offers. It handles tasks like copying entire directory trees, moving files across filesystems, removing directories recursively, and creating or extracting archives in various formats. If you’re writing scripts that manipulate files, shutil is almost certainly part of the solution.
Most shutil functions work with path-like objects (strings or paths from pathlib), making them compatible with modern Python code.
Copying Files
copy()
The shutil.copy() function copies a file’s contents and permission mode to another file or into a directory.
shutil.copy(src, dst, follow_symlinks=True)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
src | path-like | — | Source file path |
dst | path-like | — | Destination file or directory |
follow_symlinks | bool | True | If False and src is a symlink, create a symlink instead of copying the file |
Returns: The path to the newly created file.
Examples:
import shutil
# Copy a file
shutil.copy('source.txt', 'destination.txt')
# Copy into a directory (filename is derived from src)
shutil.copy('file.txt', 'myfolder/')
# Copy without following symlinks
shutil.copy('link.txt', 'output.txt', follow_symlinks=False)
This preserves the file’s permission mode but not metadata like creation time. For preserving all metadata, use shutil.copy2() instead.
Gotchas:
- If
dstis a directory that already contains a file with the same name, it gets replaced silently. shutil.copy()cannot copy special files like device nodes or named pipes.
Errors: Raises OSError if the source file cannot be read, the destination cannot be written, or if the destination is a directory and the filesystem doesn’t support replacement.
copytree()
This function recursively copies an entire directory tree.
shutil.copytree(src, dst, symlinks=False, ignore=None, copy_function=shutil.copy2,
ignore_dangling_symlinks=False, dirs_exist_ok=False)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
src | path-like | — | Source directory path |
dst | path-like | — | Destination directory path |
symlinks | bool | False | If True, preserve symlinks as symlinks; otherwise copy file contents |
ignore | callable | None | Callback to ignore files/directories during copy |
copy_function | callable | shutil.copy2 | Function to use for copying files |
ignore_dangling_symlinks | bool | False | Ignore dangling symlinks when symlinks=False |
dirs_exist_ok | bool | False | If True, allow destination to already exist |
Returns: The destination directory path.
Examples:
import shutil
# Basic directory copy
shutil.copytree('source_folder', 'destination_folder')
# Copy ignoring certain patterns
import glob
shutil.copytree('src', 'dst', ignore=glob.ignore_patterns('*.pyc', '__pycache__'))
# Allow destination to already exist (Python 3.8+)
shutil.copytree('src', 'dst', dirs_exist_ok=True)
The ignore parameter is handy for skipping files like cache directories or compiled files.
Errors: Raises OSError if directories cannot be created or files cannot be copied. Also raises shutil.Error when errors occur during the copy process — this exception contains a list of 3-tuples with (source, destination, exception) for each failed item.
Moving and Removing
move()
Moves a file or directory to another location.
shutil.move(src, dst, copy_function=shutil.copy2)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
src | path-like | — | Source file or directory |
dst | path-like | — | Destination path |
copy_function | callable | shutil.copy2 | Function to use when copy is needed |
Returns: The destination path.
Examples:
import shutil
# Move a file
shutil.move('file.txt', 'new_location.txt')
# Move a directory
shutil.move('myfolder/', 'new_folder/')
# Move into an existing directory
shutil.move('file.txt', 'existing_folder/')
The behavior with directories is slightly complex: if dst is an existing directory, src moves inside it. If dst exists but isn’t a directory, it may be overwritten depending on the OS.
Errors: Raises OSError if the source doesn’t exist, the destination cannot be written, or if both source and destination are on different filesystems and the copy fails.
rmtree()
Removes an entire directory tree. Use with caution.
shutil.rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path | path-like | — | Directory to remove |
ignore_errors | bool | False | Ignore errors during removal |
onerror | callable | None | Callback for handling exceptions |
dir_fd | int | None | Directory file descriptor |
Examples:
import shutil
# Remove a directory tree
shutil.rmtree('old_folder')
# Ignore errors (won't raise exceptions)
shutil.rmtree('folder', ignore_errors=True)
# Custom error handling
def handle_error(func, path, exc_info):
print(f"Failed to delete {path}: {exc_info[1]}")
shutil.rmtree('folder', onerror=handle_error)
Python 3.3+ includes symlink attack protection on platforms that support fd-based functions. Check shutil.rmtree.avoids_symlink_attacks to see if your platform is protected.
Errors: Raises OSError if the directory cannot be removed (unless ignore_errors=True). On Windows, may fail on read-only files unless handled with onerror.
Archiving
make_archive()
Creates compressed archive files in various formats.
shutil.make_archive(base_name, format, root_dir=None, base_dir=None, dry_run=False,
owner=None, group=None, logger=None)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
base_name | str | — | Name of archive (without extension) |
format | str | — | Archive format: “zip”, “tar”, “gztar”, “bztar”, “xztar”, “zstdtar” |
root_dir | path-like | None | Root directory of archive (becomes archive root) |
base_dir | path-like | None | Directory within root_dir to archive from |
dry_run | bool | False | If True, don’t create archive but log operations |
owner | str | None | Owner name for tar archives |
group | str | None | Group name for tar archives |
logger | object | None | Logger compatible with PEP 282 |
Returns: The path to the created archive file.
Examples:
import shutil
# Create a zip archive
shutil.make_archive('mybackup', 'zip', root_dir='/path/to/backup')
# Create a tar.gz archive
shutil.make_archive('project', 'gztar', root_dir='./project')
# Archive a specific subdirectory
shutil.make_archive('data', 'tar', root_dir='./', base_dir='data/')
The available formats depend on which compression libraries are installed: zlib for zip/gztar, bz2 for bztar, lzma for xztar, and zstandard for zstdtar.
Errors: Raises OSError if the archive cannot be created, the format is not supported, or the root directory doesn’t exist.
unpack_archive()
Extracts archives of various formats automatically.
shutil.unpack_archive(filename, extract_dir=None, format=None, *, filter=None)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
filename | path-like | — | Archive file path |
extract_dir | path-like | None | Directory to extract to (default: current directory) |
format | str | None | Force a specific format (auto-detected if None) |
filter | callable | None | Filter function for tar archives (Python 3.12+) |
Examples:
import shutil
# Extract to current directory
shutil.unpack_archive('archive.zip')
# Extract to specific directory
shutil.unpack_archive('backup.tar.gz', extract_dir='./restored')
# Force format
shutil.unpack_archive('unknown_file', format='zip')
Python’s format detection is usually reliable, but you can override it with the format parameter if needed.
Errors: Raises shutil.ReadError if the archive cannot be read, the format is not recognized, or the extraction directory cannot be created.
System Utilities
which()
Finds an executable in the system PATH.
shutil.which(cmd, mode=os.F_OK, path=None)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
cmd | str | — | Command name to find |
mode | int | os.F_OK | Permission mask (F_OK: exists, X_OK: executable) |
path | str | None | Custom PATH string (uses PATH env var if None) |
Returns: Full path to executable, or None if not found.
Examples:
import shutil
import os
# Find Python interpreter
python_path = shutil.which('python')
print(f"Found Python at: {python_path}")
# Find with specific permissions
exec_path = shutil.which('myapp', mode=os.X_OK)
# Use custom PATH
custom_path = shutil.which('gcc', path='/usr/bin:/usr/local/bin')
This is useful for building tools that need to locate external programs. On Windows, it handles the PATHEXT variable automatically, so searching for “python” finds “python.exe”.
Errors: Returns None if the command is not found in the PATH or doesn’t have the required permissions. Does not raise exceptions.
disk_usage()
Returns disk space statistics for a path.
shutil.disk_usage(path)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path | path-like | — | Path to check (file or directory) |
Returns: Named tuple with total, used, and free attributes (in bytes).
Examples:
import shutil
# Check disk usage of current directory
usage = shutil.disk_usage('.')
print(f"Total: {usage.total / (1024**3):.2f} GB")
print(f"Used: {usage.used / (1024**3):.2f} GB")
print(f"Free: {usage.free / (1024**3):.2f} GB")
# Check specific path
usage = shutil.disk_usage('/home')
print(f"Home directory: {usage.free / (1024**3):.2f} GB free")
On Unix, path must be within a mounted filesystem partition. On Windows, it can be any path.
Errors: Raises OSError if the path does not exist or is not accessible.
Common Patterns
Preserving metadata
Use shutil.copy2() instead of shutil.copy() when you need to preserve file metadata like timestamps:
import shutil
# copy2 preserves more metadata (like access/modify times)
shutil.copy2('source.txt', 'destination.txt')
Platform-specific fast copies
Since Python 3.8, shutil uses efficient kernel-level copy operations where available (sendfile on Linux, fcopyfile on macOS, CopyFileEx on Windows). This happens automatically and falls back silently if it fails.
Error handling for copytree
For shutil.copytree(), errors are collected into a shutil.Error exception with details about what failed:
import shutil
try:
shutil.copytree('src', 'dst')
except shutil.Error as e:
for src, dst, exc in e.args[0]:
print(f"Failed to copy {src} to {dst}: {exc}")