Desk Clarity

Everyday document & browser help

Copytree Destination Exists: Decide Whether You Want a Merge or a Mirror

By ON Lab · Sources checked November 8, 2026

On Python 3.8 and later, shutil.copytree(source, destination, dirs_exist_ok=True) can copy into an existing directory. It merges directory contents and can overwrite corresponding files. It does not make the destination an exact mirror or provide a no-overwrite guarantee. Choose that policy before changing the flag.

Find the actual destination first

A FileExistsError at the destination can simply mean the default copy operation refuses an existing directory. Print the resolved source and destination before touching data. Check that you have not appended the source name twice or selected a parent directory by mistake. The following example uses only new temporary paths.

from pathlib import Path
import shutil
import tempfile
with tempfile.TemporaryDirectory() as temporary:
    root = Path(temporary)
    source, destination = root / "src", root / "dst"
    source.mkdir()
    destination.mkdir()
    (source / "same.txt").write_text("new", encoding="utf-8")
    (source / "add.txt").write_text("add", encoding="utf-8")
    (destination / "same.txt").write_text("old", encoding="utf-8")
    (destination / "only-dst.txt").write_text("keep", encoding="utf-8")
    try:
        shutil.copytree(source, destination)
    except FileExistsError:
        print("default refused the existing directory")
    shutil.copytree(source, destination, dirs_exist_ok=True)
    print({p.name: p.read_text(encoding="utf-8")
           for p in destination.iterdir()})

Our Python 3.9.6 fixture produced FileExistsError for the default call. After the merge, same.txt contained new, add.txt contained add, and only-dst.txt still contained keep. The last file is the important negative control: a destination-only file was not deleted.

Choose among three different outcomes

Needed outcomeConsequence
A separate complete copyUse a new destination and keep the default refusal of an existing path.
Merge and replace matching filesOn supported Python versions, dirs_exist_ok=True may fit after reviewing the affected paths.
Preserve every existing destination fileThis flag is insufficient. Use a separately designed no-overwrite policy.
Make an exact mirrorcopytree alone does not remove destination-only entries. Do not label its result a mirror.

A “skip existing” check followed by an ordinary copy is also not a race-free no-overwrite operation. Another process can create a file between the check and the write. The separate exclusive-create file example explains that narrower boundary; it is not a full directory synchronization tool.

Handle partial results explicitly

Copying a tree is not one atomic replacement. If a later file fails, earlier files may already have been copied. Python can collect copy failures in shutil.Error. Keep the full exception details and inspect the destination rather than assuming the entire operation either succeeded or changed nothing.

When the output is meant to be a releasable package, a safer workflow is often to copy into a fresh staging directory, inspect its manifest and contents, and promote it through your application’s own release process. That process still needs an explicit concurrency and replacement policy. Automatically deleting the old destination to get past the error removes the very evidence and fallback copy you may need.

Version and filesystem limits

dirs_exist_ok was added in Python 3.8. An unexpected-keyword error calls for checking the running interpreter, not renaming folders repeatedly. Record sys.version and the imported shutil module if a wrapper or environment may be involved. Permissions, disk space and a file-versus-directory collision remain independent failure causes.

Symbolic links add another policy choice. The historical report we inspected asks about an already existing directory; it is not evidence that every link collision is solved by the flag. Our fixture used only ordinary files and directories. If a copy fails on an existing link, inspect the documented link behavior and that exact error before broadening the operation.

Verify the result you actually needed

For the fixture, three content checks prove merge behavior. For a real operation, compare the reviewed source/destination manifest, read matching output files, and keep any failure list. A successful return is not proof that a destination-only file should have survived, that every metadata field was preserved, or that the result is an exact backup. Define those requirements separately.

Method: current Python 3.13 documentation, a historical CPython copytree question, and an original Python 3.9.6 temporary-tree fixture on macOS. No real backup, synchronization, permission fault, symbolic-link tree or atomic deployment was tested.

Sources