Python shutil error handling: copy, move, copytree, rmtree (tested)
shutil raises a mix of FileNotFoundError, IsADirectoryError, FileExistsError, SameFileError and its own shutil.Error, depending on the function and the problem. We triggered each one, measured how copytree reports a tree full of failures, and wrote a copy wrapper that says which path is wrong.
1. What each shutil function raises, and when
We called copy, copyfile, copytree, move and rmtree with the inputs that usually go wrong:
import os, shutil, tempfile
d = tempfile.mkdtemp(); os.chdir(d)
open("a.txt", "w").write("hi")
os.mkdir("folder")
def show(label, fn):
try:
fn(); print(f"{label:34} OK")
except Exception as e:
print(f"{label:34} {type(e).__name__}: {e}")
show("copy: source missing", lambda: shutil.copy("nope.txt", "b.txt"))
show("copy: destination dir missing", lambda: shutil.copy("a.txt", "missing/b.txt"))
show("copy: file onto itself", lambda: shutil.copy("a.txt", "a.txt"))
show("copyfile: destination is a dir", lambda: shutil.copyfile("a.txt", "folder"))
show("copy: destination is a dir", lambda: shutil.copy("a.txt", "folder"))
show("copytree: destination exists", lambda: shutil.copytree("folder", "folder"))
show("copytree: source is a file", lambda: shutil.copytree("a.txt", "t2"))
show("move: source missing", lambda: shutil.move("nope.txt", "b.txt"))
show("move: same name already in dir", lambda: (open("folder/a.txt","w").close(), shutil.move("a.txt", "folder")))
show("rmtree: not a directory", lambda: shutil.rmtree("folder/a.txt"))
print()
for c in [shutil.SameFileError, shutil.Error]:
print(c.__name__, "->", " > ".join(k.__name__ for k in c.__mro__[1:-2]))
Output:
copy: source missing FileNotFoundError: [Errno 2] No such file or directory: 'nope.txt'
copy: destination dir missing FileNotFoundError: [Errno 2] No such file or directory: 'missing/b.txt'
copy: file onto itself SameFileError: 'a.txt' and 'a.txt' are the same file
copyfile: destination is a dir IsADirectoryError: [Errno 21] Is a directory: 'folder'
copy: destination is a dir OK
copytree: destination exists FileExistsError: [Errno 17] File exists: 'folder'
copytree: source is a file NotADirectoryError: [Errno 20] Not a directory: 'a.txt'
move: source missing FileNotFoundError: [Errno 2] No such file or directory: 'nope.txt'
move: same name already in dir Error: Destination path 'folder/a.txt' already exists
rmtree: not a directory NotADirectoryError: [Errno 20] Not a directory: 'folder/a.txt'
SameFileError -> Error > OSError > Exception
Error -> OSError > Exception
What to take from this:
- A missing source and a missing destination folder give the same error:
FileNotFoundError [Errno 2]. Onlye.filenametells you which path it was.shutil.copydoes not create parent folders; callos.makedirs(os.path.dirname(dst), exist_ok=True)first. copy()into a directory works (the file keeps its name), butcopyfile()needs a full file path and raisesIsADirectoryError.copytree()refuses an existing destination withFileExistsError. Passdirs_exist_ok=True(Python 3.8+) to merge into it.move()onto a name that already exists in the target folder raisesshutil.Error, notFileExistsError, soexcept FileExistsErrormisses it.shutil.ErrorandSameFileErrorare bothOSErrorsubclasses, soexcept OSErrorcatches everything shutil raises.
2. copytree keeps going and raises one shutil.Error at the end
When some files in a tree can't be copied, copytree() does not stop at the first one. It copies everything it can, then raises a single shutil.Error whose args[0] is a list of (src, dst, reason) tuples:
import os, shutil, tempfile
d = tempfile.mkdtemp(); os.chdir(d)
os.makedirs("src/sub")
for name in ["ok1.txt", "secret.txt", "sub/ok2.txt", "sub/secret2.txt"]:
open(f"src/{name}", "w").write(name)
os.chmod("src/secret.txt", 0o000)
os.chmod("src/sub/secret2.txt", 0o000)
try:
shutil.copytree("src", "dst")
except shutil.Error as e:
errors = e.args[0]
print(f"shutil.Error with {len(errors)} failures:")
for src, dst, why in errors:
print(" ", os.path.relpath(src, d), "->", why)
print("copied anyway:", sorted(os.path.relpath(os.path.join(r, f), "dst")
for r, _, fs in os.walk("dst") for f in fs))
Output:
shutil.Error with 2 failures:
src/secret.txt -> [Errno 13] Permission denied: 'src/secret.txt'
src/sub/secret2.txt -> [Errno 13] Permission denied: 'src/sub/secret2.txt'
copied anyway: ['ok1.txt', 'sub/ok2.txt']
Two consequences: the destination is left partially copied when the exception reaches you, and str(e) is one long list that is hard to read. Loop over e.args[0] to report each file, and delete or mark the incomplete destination if a half-copy is worse than none. For permission problems specifically, see our shutil copy/move PermissionError guide and rmtree PermissionError guide.
3. A copy wrapper with useful error messages
Since the raw errors don't say which side is missing, a small wrapper can check and re-raise with a clear message, and treat "same file" as success:
import errno, os, shutil, tempfile
def safe_copy(src, dst):
try:
return shutil.copy2(src, dst)
except shutil.SameFileError:
return dst # already there: nothing to do
except FileNotFoundError as e:
# tell apart "source missing" from "destination folder missing"
which = "source" if not os.path.exists(src) else "destination folder"
raise FileNotFoundError(errno.ENOENT, f"{which} missing", e.filename) from e
except IsADirectoryError as e:
raise IsADirectoryError(errno.EISDIR, "use copytree for directories", e.filename) from e
d = tempfile.mkdtemp(); os.chdir(d)
open("a.txt", "w").write("hi"); os.mkdir("dir")
for args in [("a.txt", "a.txt"), ("nope.txt", "x.txt"), ("a.txt", "missing/x.txt"), ("dir", "y")]:
try:
print(args, "->", safe_copy(*args))
except OSError as e:
print(args, "->", type(e).__name__, e)
Output:
('a.txt', 'a.txt') -> a.txt
('nope.txt', 'x.txt') -> FileNotFoundError [Errno 2] source missing: 'nope.txt'
('a.txt', 'missing/x.txt') -> FileNotFoundError [Errno 2] destination folder missing: 'missing/x.txt'
('dir', 'y') -> IsADirectoryError [Errno 21] use copytree for directories: 'dir'
Catch SameFileError before anything else, because it is also a shutil.Error and an OSError. Checking os.path.exists() only after the copy fails, to explain the error, is safe. Checking it before copying is not a guarantee, because the file can disappear in between. The from e keeps the original traceback attached.
Frequently asked questions
What exceptions does shutil.copy raise?
FileNotFoundError when the source or the destination folder is missing, SameFileError when copying a file onto itself, PermissionError for permission problems, and IsADirectoryError when the source is a directory.
What is shutil.Error?
An OSError subclass raised by copytree (with a list of (src, dst, reason) tuples in args[0]) and by move when the destination already exists. SameFileError is a subclass of it.
Does shutil.copytree stop at the first error?
No. It copies everything it can and then raises one shutil.Error listing every failure, leaving a partial copy behind.
How do I make copytree work when the destination exists?
Pass dirs_exist_ok=True (Python 3.8+). Without it, copytree raises FileExistsError [Errno 17].
Written with AI assistance. Measured on 2026-10-22 on Debian with Python 3.13.5: shutil.copy, copyfile, copytree, move and rmtree with missing sources, missing destination folders, existing destinations, the same file, and unreadable (0o000) files inside a tree. Every block was run verbatim and its output pasted unedited. PlainHeaders is our extension; Python is free.