Copy Files with Python `shutil`

Learn how to use Python's shutil module to copy, move, and delete files and directories with high-level file operations.

7 min read

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.

pythonpython
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.csv

shutil.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.

pythonpython
import shutil
 
shutil.copy("source.txt", "dest.txt")
 
shutil.copy2("source.txt", "dest_with_metadata.txt")
FunctionContentPermissionsTimestamps
shutil.copy(src, dst)YesYesNo
shutil.copy2(src, dst)YesYesYes
shutil.copyfile(src, dst)YesNoNo

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.

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

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

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

pythonpython
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.

pythonpython
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.

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

pythonpython
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.

pythonpython
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.

pythonpython
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 GB

usage.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.

pythonpython
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_name

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

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

pythonpython
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

Rune AI

Key Insights

  • Use shutil.copy(src, dst) to copy a file and shutil.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.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between shutil.copy and shutil.copy2?

`shutil.copy()` copies the file content and permission bits. `shutil.copy2()` also copies metadata like the last modification time. Use `copy2()` when you need to preserve timestamps for backup or synchronization scripts.

How do I delete a directory that is not empty?

Use `shutil.rmtree(path)`. Unlike `os.rmdir()`, which only removes empty directories, `rmtree()` deletes the directory and everything inside it. Use it carefully; the deletion is permanent and does not go to the trash.

Does shutil.move work across different filesystems?

Yes. `shutil.move()` first tries `os.rename()`, which is fast but only works on the same filesystem. If that fails, it falls back to copying the file to the destination and deleting the source, which works across different drives or partitions.

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.