Skip to main content

How to Copy Directory Trees in Python

Copying entire folder structures (including subdirectories, files, and metadata) is a common task for backups, deployments, and file synchronization. Python's shutil module provides robust functions for recursive directory copying.

Copy Entire Directory with shutil.copytree()

The copytree() function recursively copies a directory and all its contents.

import shutil

source = "./project_v1"
destination = "./project_backup"

# Copy entire directory tree
shutil.copytree(source, destination)

print(f"Copied {source} to {destination}")
warning

By default, copytree() requires the destination directory to not exist. If it already exists, a FileExistsError is raised.

Copy into Existing Directory with dirs_exist_ok

Since Python 3.8, the dirs_exist_ok parameter allows copying into existing directories, merging contents.

import shutil

source = "./updates"
destination = "./project"

# Merge into existing directory, overwriting conflicts
shutil.copytree(source, destination, dirs_exist_ok=True)

print("Files merged successfully")

Behavior with dirs_exist_ok=True

ScenarioResult
File exists in destinationOverwritten with source file
File only in sourceCopied to destination
File only in destinationLeft unchanged
Subdirectory existsMerged recursively

Filter Files with ignore Patterns

Exclude specific files or directories during copying using ignore_patterns().

import shutil

source = "./project"
destination = "./project_clean"

# Exclude compiled files, caches, and temporary files
shutil.copytree(
source,
destination,
ignore=shutil.ignore_patterns(
'*.pyc',
'__pycache__',
'*.tmp',
'.git',
'node_modules'
)
)

print("Copied without excluded patterns")

Create Custom Ignore Functions

For complex filtering logic, define a custom ignore function.

import shutil
import os

def ignore_large_files(directory, files):
"""Ignore files larger than 10MB."""
ignored = []
for file in files:
filepath = os.path.join(directory, file)
if os.path.isfile(filepath):
size_mb = os.path.getsize(filepath) / (1024 * 1024)
if size_mb > 10:
ignored.append(file)
return ignored

shutil.copytree(
"./source",
"./destination",
ignore=ignore_large_files
)

Combine Multiple Ignore Conditions

import shutil
import os

def custom_ignore(directory, files):
"""Ignore hidden files, temp files, and specific directories."""
ignored = set()

# Ignore hidden files (starting with .)
ignored.update(f for f in files if f.startswith('.'))

# Ignore temp files
ignored.update(f for f in files if f.endswith(('.tmp', '.bak')))

# Ignore specific directories
ignored.update(f for f in files if f in ['__pycache__', 'node_modules'])

return ignored

shutil.copytree("./source", "./destination", ignore=custom_ignore)

Copy Individual Files with copy2()

For granular control or custom copying logic, use copy2() which preserves file metadata.

import shutil
import os

source_dir = "./documents"
dest_dir = "./backup"

# Ensure destination exists
os.makedirs(dest_dir, exist_ok=True)

# Copy only text files
for filename in os.listdir(source_dir):
if filename.endswith(".txt"):
source_path = os.path.join(source_dir, filename)
dest_path = os.path.join(dest_dir, filename)

# copy2 preserves timestamps and metadata
shutil.copy2(source_path, dest_path)
print(f"Copied: {filename}")

Difference Between copy() and copy2()

import shutil
import os

source_file = "./data.txt"

# copy() - copies content and permissions only
shutil.copy(source_file, "./copy_basic.txt")

# copy2() - copies content, permissions, AND timestamps
shutil.copy2(source_file, "./copy_full.txt")

# Verify timestamps
original_stat = os.stat(source_file)
copy_stat = os.stat("./copy_basic.txt")
copy2_stat = os.stat("./copy_full.txt")

print(f"Original mtime: {original_stat.st_mtime}")
print(f"copy() mtime: {copy_stat.st_mtime}") # Different
print(f"copy2() mtime: {copy2_stat.st_mtime}") # Same as original
FunctionContentPermissionsTimestamps
copy()
copy2()
copyfile()

Handle Errors During Copying

Use error handling to manage permission issues or missing files.

import shutil

source = "./protected_folder"
destination = "./backup"

try:
shutil.copytree(source, destination)
print("Copy completed successfully")
except FileExistsError:
print(f"Destination {destination} already exists")
except PermissionError:
print("Permission denied - check file access rights")
except shutil.Error as e:
print(f"Copy error occurred: {e}")
except OSError as e:
print(f"OS error: {e}")

Continue Copying Despite Errors

import shutil

def copy_with_errors(src, dst):
"""Copy directory, collecting errors instead of stopping."""
errors = []

def error_handler(function, path, excinfo):
errors.append((path, excinfo[1]))

# Note: copytree doesn't have built-in error handler
# Use custom implementation for error collection
try:
shutil.copytree(src, dst, dirs_exist_ok=True)
except shutil.Error as e:
errors.extend(e.args[0])

return errors

errors = copy_with_errors("./source", "./destination")
if errors:
print(f"Completed with {len(errors)} errors:")
for path, error in errors:
print(f" {path}: {error}")

Copy with Progress Tracking

For large directories, implement progress tracking with a custom copy function.

import shutil
import os

def copytree_with_progress(src, dst):
"""Copy directory tree with progress output."""
# Count total files first
total_files = sum(len(files) for _, _, files in os.walk(src))
copied = 0

def copy_function(source, destination):
nonlocal copied
shutil.copy2(source, destination)
copied += 1
print(f"\rProgress: {copied}/{total_files} files", end="")

shutil.copytree(src, dst, copy_function=copy_function)
print(f"\nCompleted: {copied} files copied")

copytree_with_progress("./large_project", "./backup")
tip

For very large directories, consider using rsync via subprocess for better performance and resume capability.

Quick Reference

TaskFunctionExample
Copy directory recursivelycopytree(src, dst)Full backup
Merge into existingcopytree(src, dst, dirs_exist_ok=True)Update/sync
Exclude patternscopytree(src, dst, ignore=ignore_patterns(...))Skip cache files
Copy single file with metadatacopy2(src, dst)Preserve timestamps
Copy single file basiccopy(src, dst)Content only

Conclusion

Use shutil.copytree() for recursive directory copying. It handles the complexity of walking directory trees and preserving structure. Add dirs_exist_ok=True for merge operations and ignore_patterns() to filter unwanted files. For individual files, prefer copy2() over copy() to preserve timestamps and metadata, which is essential for backup integrity and file synchronization.