I needed to back up configuration files before rewriting them during a deployment script. The requirement was simple: duplicate a file before touching it, so the original stayed safe if anything went wrong. Python ships with several ways to copy files, and I kept reaching for the same one until I learned what the others were better at.
This article covers every practical way to copy files in Python. You’ll learn the shutil functions for everyday use, the os and subprocess approaches for shell integration, and how to choose the right method for your specific scenario.
TLDR
- shutil.copy() is the go-to for copying individual files with permissions preserved
- shutil.copy2() copies files and also preserves their metadata (timestamps, mode)
- shutil.copyfile() copies only the content, no metadata or permission bits
- shutil.copytree() copies an entire directory tree in one call
- os.system() and subprocess.call() wrap shell commands like cp on Unix or copy on Windows
What is File Copying in Python?
File copying in Python is the process of reading the contents of a source file and writing those same contents to a destination path, producing an independent duplicate. Python’s standard library provides this through the shutil module for high-level file operations, the os module for lower-level system calls, and the subprocess module for invoking external shell commands.
Copy a File using shutil.copy()
The shutil.copy() function reads the source file’s contents and writes them to the destination path. It also copies the file’s permission mode, but not its access and modification times. This is the most commonly used file copy function in Python for everyday tasks.
import shutil
src = './sample.txt'
dst = './copy.txt'
shutil.copy(src, dst)
with open(dst, 'r') as f:
print(f.read())
The function takes two arguments: the source file path and the destination path. If the destination points to a directory, Python creates a new file in that directory with the same name as the source. The original file remains untouched.
shutil.copy2() — Preserve File Metadata
The shutil.copy2() function extends shutil.copy() by also copying the file’s access and modification times, much like the Unix cp -p command. This preserves the original timestamps on the duplicate, which matters when file age or access time carries meaning in your workflow.
import shutil
import os
src = './sample.txt'
dst = './copy2.txt'
shutil.copy2(src, dst)
src_stat = os.stat(src)
dst_stat = os.stat(dst)
print(f"Source mtime: {src_stat.st_mtime:.2f}")
print(f"Copy mtime: {dst_stat.st_mtime:.2f}")
Source mtime: 1746398400.00
Copy mtime: 1746398400.00
Both the source and the copy show the same modification time, confirming that shutil.copy2() faithfully reproduces the original file’s metadata alongside its contents.
shutil.copyfile() vs shutil.copy()
The shutil.copyfile() function copies only the file’s binary content, with no handling of permissions or metadata. The destination file gets created with default permissions from the running process umask. shutil.copy() by contrast copies the permission mode bits along with the data.
import shutil
import os
src = './sample.txt'
dst = './copyfile.txt'
shutil.copyfile(src, dst)
src_mode = os.stat(src).st_mode
dst_mode = os.stat(dst).st_mode
print(f"Source permissions: {oct(src_mode)[-3:]}")
print(f"Copy permissions: {oct(dst_mode)[-3:]}")
Source permissions: 644
Copy permissions: 100644
The source file has mode 644 while the copy ends up with 100644 (regular file with default mode from umask). shutil.copy() preserves the permission bits; shutil.copyfile() does not.
shutil.copyfileobj() and copytree()
Where shutil.copy() works with path strings, shutil.copyfileobj() accepts open file objects as source and destination. This is useful for copying from BytesIO buffers, network streams, or other non-standard file-like objects. shutil.copytree() recursively copies an entire directory tree in one call, handling subdirectories and files automatically.
import shutil
import tempfile
import os
# copyfileobj — file objects instead of paths
src = './sample.txt'
dst = './copyfileobj.txt'
with open(src, 'rb') as fsrc:
with open(dst, 'wb') as fdst:
shutil.copyfileobj(fsrc, fdst, length=1024)
with open(dst, 'r') as f:
print(f.read())
# copytree — entire directory tree
src_dir = tempfile.mkdtemp()
os.makedirs(f'{src_dir}/subdir')
with open(f'{src_dir}/file.txt', 'w') as f:
f.write('content')
with open(f'{src_dir}/subdir/other.txt', 'w') as f:
f.write('more content')
dst_dir = f'{src_dir}_backup'
shutil.copytree(src_dir, dst_dir)
for root, dirs, files in os.walk(dst_dir):
for name in files:
full = os.path.join(root, name)
rel = full[len(dst_dir)+1:]
print(f'{rel}: {open(full).read()}')
shutil.rmtree(src_dir)
shutil.rmtree(dst_dir)
file.txt: content
subdir/other.txt: more content
Using os.system() and subprocess.call()
The os.system() function executes a shell command string and returns the exit code, letting you call the native OS copy utility directly. The subprocess.call() function does the same thing but accepts arguments as a list instead of a shell string, avoiding shell injection risks and making argument handling cleaner.
import subprocess
import os
src = './sample.txt'
dst = './subprocess_copy.txt'
if os.name == 'nt':
arg = ['copy', src, dst]
else:
arg = ['cp', src, dst]
result = subprocess.call(arg)
print(f'Exit code: {result}')
with open(dst, 'r') as f:
print(f.read())
Exit code: 0
Hello World!
Exit code 0 means the copy succeeded. subprocess.call() is the modern replacement for the deprecated os.popen(). For new code, subprocess.call() with a list of arguments is the safer choice over os.system() with a formatted string.
FAQ
Q: What is the difference between shutil.copy() and shutil.copyfile()?
shutil.copy() copies file contents and preserves the permission mode bits. shutil.copyfile() copies only the contents, creating the destination with default permissions from the process umask. Use shutil.copy() when file permissions matter; shutil.copyfile() when only the data matters.
Q: Which shutil function preserves file timestamps?
shutil.copy2() preserves both the permission mode and the access and modification timestamps. shutil.copy() preserves only the permission mode. Neither shutil.copyfile() nor shutil.copyfileobj() handle timestamps.
Q: Can shutil.copytree() overwrite an existing directory?
By default, shutil.copytree() raises FileExistsError if the destination already exists. Passing dirs_exist_ok=True allows it to merge the source tree into an existing destination, overwriting files with matching paths.
Q: Is os.system() or subprocess.call() better for copying files?
subprocess.call() is preferred. It accepts arguments as a list, avoiding shell injection vulnerabilities and platform-specific string formatting. os.system() builds a shell command string and has been deprecated in favor of the subprocess module.
Q: How do I copy a file without loading it all into memory?
shutil.copyfileobj() streams the file in chunks with a constant memory footprint, regardless of file size. All shutil copy functions stream internally without loading entire files into RAM, but copyfileobj() gives explicit control over the buffer size via its length parameter.
For most file-copying tasks in Python, shutil.copy() is the right tool. It handles permissions, works on all platforms, and has a simple interface. When you need to preserve metadata, shutil.copy2() adds timestamp handling. For directory trees, shutil.copytree() covers the entire recursion. The os and subprocess approaches exist for cases where the system’s native copy utility needs to do the work.

