How to copy a file in Python
shutil offers four functions for this and they differ in what they carry
across. As of Python 3.14 there is also a pathlib method, which every answer
written before then predates.
The new one first
from pathlib import Path
Path("source.txt").copy("target.txt")
Path.copy() arrived in
Python 3.14, along with Path.copy_into() and Path.move(). Before that
pathlib could do almost everything with paths except copy, which is why
every older answer reaches for shutil.
Note the default:
src.copy(target) : -rw-r--r-- mtime=<now>
copy(preserve_metadata=True) : -rwxr-x--- mtime=<original>
Path.copy() copies the contents only unless you ask for more. So it
behaves like shutil.copyfile, not like copy2.
And for copying into a directory rather than to a name:
src.copy_into(a_directory) : created True
If you are on 3.13 or earlier — which, realistically, most projects are — read on.
The four shutil functions
The difference is what they preserve. Source here is -rwxr-x--- with an mtime
one day in the past:
source : -rwxr-x--- mtime=1788078255
copyfile -> -rw-r--r-- mtime=1788164655
copy -> -rwxr-x--- mtime=1788164655
copy2 -> -rwxr-x--- mtime=1788078255
copyfile— contents. The destination gets fresh permissions from your umask.copy— contents and permission bits.copy2— contents, permissions and timestamps.
copy2 is the closest to cp -p, and the one to use when the copy is meant to
stand in for the original — a backup, a staged deploy, anything where an
mtime-based tool will look at it later.
The difference that is not about metadata
This one catches people more often:
shutil.copy(src, a_directory) : created True
shutil.copyfile(src, a_directory) -> IsADirectoryError: [Errno 21] Is a directory
copy and copy2 accept a directory as the destination and keep the
original filename. copyfile requires a full target path.
So shutil.copy(f, "/backup/") works and shutil.copyfile(f, "/backup/") does
not, regardless of what you wanted preserved.
Copying a file onto itself
copyfile(src, src) -> SameFileError: ... are the same file
SameFileError
is a subclass of OSError, so you can catch just this case. Worth knowing if
you build source and destination paths separately and they might coincide —
without the check you would truncate the file before reading it.
What copy2 still does not copy
copy2 uses copystat, which covers mode, timestamps and, where supported,
flags and extended attributes. It does not cover ownership.
Copying a file owned by someone else leaves you as the owner of the copy.
Changing that needs os.chown and, in practice, root. If you are moving files
around as a service account and the owner matters, that is a step you have to
write yourself.
Whole directories
copytree : True
again -> FileExistsError
with dirs_exist_ok=True : fine (added in 3.8)
copytree
refuses an existing destination by default. dirs_exist_ok=True merges into
it. It also takes an ignore= callable, for which shutil.ignore_patterns
covers the common case:
shutil.copytree(src, dst, ignore=shutil.ignore_patterns("*.pyc", "__pycache__"))
When you have file objects rather than paths
with open(src, "rb") as fsrc, open(dst, "wb") as fdst:
shutil.copyfileobj(fsrc, fdst)
copyfileobj : 'hello\n'
copyfileobj
works on anything file-like, so it is the one for an HTTP response body, an
upload stream, or a member of a tar or zip archive. It copies in chunks, so
the file size does not become your memory usage.
What not to do
The one-liner that turns up in older code:
open(dst, "w").write(open(src).read())
Three problems. It reads the whole file into memory. It leaks both file handles. And in text mode it decodes and re-encodes, which rewrites line endings and mangles anything that is not text:
reading a binary file as text -> UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 ...
Here it raised, which is the good outcome. On a file that happens to decode cleanly it would succeed and quietly hand you a different file.
One reason to prefer shutil to hand-rolling it
8 MiB, shutil.copyfile : 1.8 ms
8 MiB, manual 64K chunks : 2.1 ms
Since Python 3.8, shutil uses the platform’s zero-copy calls where they exist
— os.sendfile and copy_file_range on Linux, fcopyfile on macOS. The data
does not have to travel through your process at all.
The margin above is small because the file was in the page cache and the
container filesystem is an overlay. The point is not that shutil is
dramatically faster; it is that a hand-written chunk loop cannot be faster, and
has more ways to be wrong.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).