The shutil module in the Python standard library provides high-level operations for copying, moving, and deleting files and directories. It builds on lower-level modules like the os module and pathlib but handles edge cases like metadata preservation, recursive directory trees, and cross-filesystem moves automatically.
import shutil
from pathlib import Path
src = Path("data/input.csv")
dst = Path("backups/input_backup.csv")
shutil.copy(src, dst)
print(f"Copied to {dst}") # Copied to backups/input_backup.csvshutil.copy(src, dst) copies the file content and permission bits. The destination can be a file path or a directory; if it is a directory, the file keeps its original name inside that directory.
Copying files
Different copy situations call for different amounts of metadata preservation. The shutil module provides three copy functions with different levels of metadata preservation, so you can pick the cheapest one that still meets your needs.
import shutil
shutil.copy("source.txt", "dest.txt")
shutil.copy2("source.txt", "dest_with_metadata.txt")| Function | Content | Permissions | Timestamps |
|---|---|---|---|
| shutil.copy(src, dst) | Yes | Yes | No |
| shutil.copy2(src, dst) | Yes | Yes | Yes |
| shutil.copyfile(src, dst) | Yes | No | No |
Use shutil.copy() for most cases. Use shutil.copy2() when you need to preserve the last-modified time, such as in backup or synchronization scripts. Use shutil.copyfile() when you want only the raw content with no metadata at all.
All three functions return the destination path as a string.
Copying entire directories
shutil.copytree() recursively copies a directory and everything inside it.
import shutil
shutil.copytree("data", "data_backup")This copies the data directory and all its files and subdirectories to data_backup. The destination must not already exist unless you pass dirs_exist_ok=True:
shutil.copytree("data", "data_backup", dirs_exist_ok=True)With dirs_exist_ok=True, existing files in the destination are overwritten. Use this for incremental backups where you want to refresh files without deleting the destination first.
You can also filter which files to include or exclude:
import shutil
def ignore_logs(directory, contents):
return [item for item in contents if item.endswith(".log")]
shutil.copytree("project", "project_backup", ignore=ignore_logs)The ignore function receives the current directory path and a list of its contents, and returns a list of names to skip. shutil also provides shutil.ignore_patterns() for glob-style filtering:
shutil.copytree("project", "project_backup", ignore=shutil.ignore_patterns("*.log", "__pycache__"))Moving files and directories
Copying and deleting separately would leave a window where both copies exist or neither does if something fails midway. shutil.move() relocates a file or directory in one call, and it works across filesystems.
import shutil
shutil.move("old_name.txt", "new_name.txt")
shutil.move("processed.csv", "archive/processed.csv")On the same filesystem, shutil.move() uses os.rename(), which is fast. Across filesystems, it copies the file and deletes the source, which is slower but works everywhere.
If the destination is a directory, the source is moved inside it with its original name. If the destination is a file path, the source is renamed.
Deleting files and directories
Use shutil.rmtree() to delete a directory and everything inside it.
import shutil
shutil.rmtree("temp_data")Unlike os.rmdir(), which only removes empty directories, rmtree() removes the directory and all its contents recursively. This is permanent; the files do not go to a trash or recycle bin.
To handle errors during deletion, use the onexc callback:
import shutil
import os
def handle_error(func, path, exc):
print(f"Error removing {path}: {exc}")
os.chmod(path, 0o777)
func(path)
shutil.rmtree("temp_data", onexc=handle_error)The callback receives the function that failed, the path that caused the error, and the exception instance itself. This is useful for handling permission errors on read-only files. An older onerror parameter still exists for backward compatibility, but it passes an exc_info tuple instead of the exception directly, and the official documentation now recommends onexc for new code.
Creating archives
shutil.make_archive() creates zip or tar archives from a directory.
import shutil
shutil.make_archive("project_backup", "zip", "project")This creates project_backup.zip containing the contents of the project directory. Supported formats include "zip", "tar", "gztar" (tar.gz), "bztar" (tar.bz2), and "xztar" (tar.xz).
The base name should not include the extension; shutil appends it based on the format. The third argument is the root directory to archive.
Checking disk usage
shutil.disk_usage() returns total, used, and free space on a filesystem.
import shutil
usage = shutil.disk_usage("/")
print(f"Total: {usage.total // (1024**3)} GB") # Total: 500 GB
print(f"Used: {usage.used // (1024**3)} GB") # Used: 320 GB
print(f"Free: {usage.free // (1024**3)} GB") # Free: 180 GBusage.total, usage.used, and usage.free are in bytes. Divide by 1024**3 for gigabytes. This is useful before starting a large copy or archive operation to confirm there is enough space.
Practical example: automated backup script
Combine several shutil functions into a backup script.
import shutil
import sys
from pathlib import Path
from datetime import datetime
source = Path("project")
backup_root = Path("backups")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"{source.name}_{timestamp}"
backup_path = backup_root / backup_nameWith the destination name ready, check that the backup drive has enough free space before copying anything, since a partial backup is worse than no backup at all:
usage = shutil.disk_usage(backup_root)
if usage.free < sum(f.stat().st_size for f in source.rglob("*") if f.is_file()):
print(f"Not enough space. Free: {usage.free // (1024**3)} GB")
sys.exit(1)Once there is enough free space confirmed, the script copies the project, compresses that copy into a single archive, and discards the uncompressed copy it no longer needs:
shutil.copytree(source, backup_path)
shutil.make_archive(str(backup_path), "gztar", str(backup_path))
shutil.rmtree(backup_path)
print(f"Backup created: {backup_path}.tar.gz")The script creates a timestamped copy of the project directory, checks for sufficient disk space, archives the copy as a compressed tar file, and removes the temporary copy. The final result is a single compressed archive.
Common mistakes
Forgetting that rmtree is permanent. shutil.rmtree() does not move files to the trash. Test your paths carefully, especially when the path comes from user input or a configuration file.
Using copytree on an existing destination without dirs_exist_ok. shutil.copytree() raises a FileExistsError if the destination already exists. Add dirs_exist_ok=True when you intend to refresh an existing backup directory.
Confusing copy and copy2 for backups. If you need timestamps preserved for incremental backup logic, use shutil.copy2(). Using shutil.copy() will reset the modification time, which can confuse tools that compare files by timestamp.
Using make_archive with the wrong base name. The archive name should not include the extension. shutil.make_archive("backup", "zip", "data") produces backup.zip, not backup.zip.zip.
Rune AI
Key Insights
- Use
shutil.copy(src, dst)to copy a file andshutil.copy2()to also preserve timestamps. - Use
shutil.copytree(src, dst)to copy an entire directory. - Use
shutil.move(src, dst)to move or rename files and directories. - Use
shutil.rmtree(path)to delete a directory and all its contents. - Use
shutil.make_archive()to create zip or tar archives. - Use
shutil.disk_usage(path)to check available disk space.
Frequently Asked Questions
What is the difference between shutil.copy and shutil.copy2?
How do I delete a directory that is not empty?
Does shutil.move work across different filesystems?
Conclusion
shutil is the go-to module for high-level file operations in Python. Use shutil.copy() and shutil.copy2() to duplicate files, shutil.move() to relocate them, shutil.rmtree() to delete directories, and shutil.make_archive() to create compressed archives. Always test your file operations on sample data before running them in production.
More in this topic
Format Dates and Times in Python
Learn how to use strftime and strptime to format dates as strings and parse date strings into Python datetime objects.
What Is the Python Standard Library?
Learn what the Python standard library is, why it matters, and how to use its built-in modules without installing anything extra.
Encode and Decode Base64 Data in Python
Learn how to use Python's base64 module to encode binary data as text and decode Base64 strings back to bytes.