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, ...).