Python’s built-in shutil module is the standard, go-to tool for copying files and directories, offering a higher-level interface than manually reading and writing file bytes yourself. It’s part of the standard library, so no installation is required — just import it and start. Here’s how to use it for the most common file-copying scenarios.
Copying a Single File: shutil.copy()
The most basic and commonly used function for duplicating a file is shutil.copy(). It takes a source path and a destination, and the destination can be either a folder (which keeps the original filename) or a full file path (which lets you rename the copy at the same time):
import shutil
# Copy to a folder, keeping the original filename
shutil.copy('project.txt', 'backup/')
# Copy and rename at the same time
shutil.copy('project.txt', 'backup/project_final.txt')
shutil.copy() preserves the file’s contents and basic permission mode, but it does not preserve metadata like the original creation or modification timestamps. For most everyday scripting and automation tasks, this is perfectly fine.
Preserving Metadata: shutil.copy2()
If timestamps and other metadata genuinely matter — for backups, archival workflows, or any situation where “when was this file last modified” needs to stay accurate — use shutil.copy2() instead:
import shutil
shutil.copy2('example.txt', 'backup/example.txt')
This works identically to shutil.copy() but also copies over metadata like modification and access timestamps, making it the better default for backup-focused scripts.
Copying Only File Content: shutil.copyfile()
If you specifically want to copy just the raw content of a file, without permissions or any metadata handling, shutil.copyfile() does exactly that:
import shutil
shutil.copyfile('example.txt', 'backup/example_copy.txt')
One important difference: unlike copy() and copy2(), both the source and destination arguments to copyfile() must be file paths — if the destination is a directory, it raises an IsADirectoryError rather than automatically placing the file inside it.
Copying an Entire Folder: shutil.copytree()
To copy a whole directory — including all its subdirectories, files, and symlinks — use shutil.copytree(), which is effectively the Python equivalent of running cp -r on Unix systems:
import shutil
shutil.copytree('source_folder', 'destination_folder')
An important detail: the destination folder must not already exist when you call copytree() — it will be created automatically as part of the copy. If the destination already exists, this raises an error, unless you’re running a Python version that supports the dirs_exist_ok=True argument to allow merging into an existing folder.
Excluding Specific Files When Copying a Directory
If you need to skip certain files or file types while copying an entire directory tree, copytree() accepts an ignore argument:
import shutil
shutil.copytree(
'source_folder',
'destination_folder',
ignore=shutil.ignore_patterns('*.tmp', '*.log')
)
This copies everything in the source directory except files matching the given patterns — useful for skipping temporary files, logs, or cache directories during a backup or deployment script.
Copying Just File Metadata: shutil.copystat()
If you need finer-grained control — copying only the metadata (permissions, timestamps) without touching the file’s actual contents — shutil.copystat() handles that specific case:
import shutil
shutil.copystat('source.txt', 'destination.txt')
This is useful in workflows where you’ve already copied a file’s content through some other method and need to separately sync its metadata afterward.
Handling Real-World Edge Cases
A few practical safeguards are worth building into any file-copying script that runs unattended:
- Check available disk space before copying large files, using
shutil.disk_usage() alongside os.path.getsize(), to avoid a failed copy partway through
- Ensure destination directories exist before copying into them —
shutil.copy() will raise a FileNotFoundError if you try to copy into a path with a non-existent parent directory
- Wrap copy operations in try/except blocks to handle permission errors, missing files, or interrupted copies gracefully, rather than letting a script crash outright mid-operation
import shutil
import os
try:
if not os.path.exists('backup'):
os.makedirs('backup')
shutil.copy2('important_file.txt', 'backup/')
except (IOError, OSError) as e:
print(f"Copy failed: {e}")
Quick Reference: Which Function to Use
- Single file, no metadata needed:
shutil.copy()
- Single file, metadata matters (backups):
shutil.copy2()
- Single file, content only, strict path requirements:
shutil.copyfile()
- Entire folder or directory tree:
shutil.copytree()
- Metadata only, no content:
shutil.copystat()
Join The Discussion
Have you built automation scripts around shutil for backups, deployments, or general file organization? Share what functions you reach for most, or a tricky edge case — permission errors, existing destination folders, huge datasets — that you had to work around. If you’re currently building out a file-copying script and running into an issue, feel free to ask — there’s a good chance someone here has hit the same snag.