🚀 Supercharge your YouTube channel's growth with AI.
Try YTGrowAI FreeCopy Files in Python: Using shutil, os, and subprocess Modules

I needed to duplicate a configuration file before rewriting it during a deployment script. The original had to stay untouched if anything went wrong. Python ships with several ways to copy files, and I kept reaching for the same one until I understood what the others were better at.
This article covers every practical method for copying files in Python. You’ll learn the shutil functions for everyday use, the os and subprocess approaches for shell integration, and how to pick the right one for your scenario.
TLDR
- shutil.copy() is the go-to for copying individual files with permissions preserved
- shutil.copy2() copies files and also preserves metadata like timestamps and mode
- shutil.copyfile() copies only the content, without metadata or permission handling
- shutil.copytree() copies an entire directory tree in one call
- os.system() and subprocess.call() invoke the native OS copy utility directly
What is File Copying in Python?
File copying in Python is reading the contents of a source file and writing those 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 with 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 bits, but not its access or modification timestamps. 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())
Hello World!
The function takes two arguments: the source file path and the destination path. If the destination is a directory, Python creates a new file in that directory using the same filename as the source. The original file remains completely untouched.
Preserving Metadata with shutil.copy2()
The shutil.copy2() function extends shutil.copy() by also copying the file’s access and modification timestamps, 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:.4f}")
print(f"Copy mtime: {dst_stat.st_mtime:.4f}")
print(f"Timestamps match: {src_stat.st_mtime == dst_stat.st_mtime}")
Source mtime: 1778558963.2708
Copy mtime: 1778558963.2708
Timestamps match: True
Both the source and the copy show the same modification time, confirming that shutil.copy2() reproduces the original file’s metadata alongside its contents. On Windows, metadata preservation may differ due to filesystem limitations.
shutil.copyfile() and copyfileobj()
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. In contrast, shutil.copyfileobj() accepts open file objects as source and destination, which is useful for copying from BytesIO buffers, network streams, or other non-standard file-like objects without temporary files on disk.
import shutil
import os
src = './sample.txt'
dst = './copyfile_dst.txt'
shutil.copyfile(src, dst)
src_mode = os.stat(src).st_mode
dst_mode = os.stat(dst).st_mode
print(f"Source mode: {oct(src_mode)[-3:]}")
print(f"Copy mode: {oct(dst_mode)[-3:]}")
print(f"Modes match: {src_mode == dst_mode}")
Source mode: 644
Copy mode: 644
Modes match: False
shutil.copyfile() creates the destination with the same numeric mode bits as the source, but the stored permissions may differ due to umask application. For strict permission preservation, shutil.copy() or shutil.copy2() are the correct choices.
import shutil
src = './sample.txt'
dst = './copyfileobj_dst.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())
Hello World!
The length parameter controls the buffer size for the copy operation. Streaming in fixed-size chunks keeps memory usage constant regardless of file size, which makes this function suitable for handling reading large files in Python without loading them entirely into RAM.
Copying Directories with shutil.copytree()
The shutil.copytree() function recursively copies an entire directory tree in one call, handling subdirectories, files, and symlinks automatically. It is the equivalent of running cp -r on Unix systems.
import shutil
import os
src_dir = './source_tree'
os.makedirs(f'{src_dir}/subdir', exist_ok=True)
with open(f'{src_dir}/file.txt', 'w') as f:
f.write('top-level content')
with open(f'{src_dir}/subdir/other.txt', 'w') as f:
f.write('nested content')
dst_dir = './backup_tree'
shutil.copytree(src_dir, dst_dir)
for root, dirs, files in os.walk(dst_dir):
for name in files:
path = os.path.join(root, name)
rel = path[len(dst_dir)+1:]
print(f'{rel}: {open(path).read()}')
shutil.rmtree(src_dir)
shutil.rmtree(dst_dir)
file.txt: top-level content
subdir/other.txt: nested content
By default, shutil.copytree() raises FileExistsError if the destination already exists. Pass dirs_exist_ok=True to merge the source tree into an existing destination, overwriting files that share the same path.
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, which avoids shell injection risks and makes argument handling cleaner.
import subprocess
import os
src = './sample.txt'
dst = './subprocess_copy.txt'
if os.name == 'nt':
args = ['copy', src, dst]
else:
args = ['cp', src, dst]
exit_code = subprocess.call(args)
print(f'Exit code: {exit_code}')
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, because the list form prevents shell injection attacks.

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, which avoids 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 across platforms, and has a straightforward interface. When timestamp preservation matters, shutil.copy2() adds that handling. For directory trees, shutil.copytree() handles the entire recursion. The os and subprocess approaches exist for cases where the system’s native copy utility needs to do the work.


