Desk Clarity

A field guide to everyday digital problems.

Developer tools / Practical guide

Python SameFileError: Different Paths Can Refer to the Same File

Diagnose Python SameFileError with file identity checks, disposable hard-link and symbolic-link examples, and an explicit skip policy.

By ON Lab5 min readSources checked November 10, 2026 (KST)

A copy job can raise shutil.SameFileError even when the source and destination strings differ. Before deleting the destination or changing permissions, establish whether both paths identify the same underlying file. A hard link and a symbolic link can give one file additional names. Removing an alias as an automatic retry is a poor default: you may remove a useful name without resolving the job's intended destination.

This guide diagnoses the default shutil.copyfile(src, dst) operation on existing local files. It separates an identity collision from an ordinary existing destination, then makes the skip-or-copy decision explicit. It does not provide an atomic no-overwrite operation or a security boundary for paths controlled by another process.

Identify the operation that actually failed

Keep the original exception class and the exact source and destination values from the failing operation. Avoid exposing private paths in shared logs. SameFileError indicates that the copy source and destination identify the same file. A distinct, writable existing destination is a different case: copyfile normally replaces its contents. Changing permissions does not turn one file into two files.

Check how the application constructs its final destination. copyfile expects a complete file path, whereas a higher-level helper might first combine a destination directory with the source basename. Diagnose that computed path, not just the directory a user selected. In a batch job, record the item that failed rather than assuming every item has the same collision.

Use os.path.samefile(source, destination) when both paths can be inspected. It compares filesystem identity rather than text spelling. Path.resolve() can expose symbolic-link targets, but it cannot make two different hard-link names look identical. An absolute-path or case-normalization comparison therefore does not replace the identity check.

Reproduce aliases without touching application files

Save the following as samefile-demo.py and run python3 samefile-demo.py. It creates its own temporary directory. The hard-link and symbolic-link steps require filesystem support and permission; a failure to create a link is a setup failure, not evidence that copying an alias succeeds.

from pathlib import Path
import os
import shutil
import tempfile

with tempfile.TemporaryDirectory() as folder:
    root = Path(folder)
    source = root / "source.txt"
    payload = b"original bytes\n"
    source.write_bytes(payload)
    hard = root / "hard.txt"
    symbolic = root / "symbolic.txt"
    os.link(source, hard)
    symbolic.symlink_to(source)

    for label, destination in [
        ("same path", source),
        ("hard link", hard),
        ("symbolic link", symbolic),
    ]:
        assert os.path.samefile(source, destination)
        try:
            shutil.copyfile(source, destination)
        except shutil.SameFileError:
            print(label, "SameFileError")
        else:
            raise AssertionError("expected identity collision")
        assert source.read_bytes() == payload

    independent = root / "independent.txt"
    independent.write_bytes(payload)
    assert not os.path.samefile(source, independent)
    shutil.copyfile(source, independent)
    assert independent.read_bytes() == payload
    print("equal bytes can belong to distinct files")

The final check matters: identical content is not identical file identity. A synchronization tool might choose to skip a distinct file after comparing content, but that is a separate optimization with its own consistency rules. Do not interpret an equal hash as proof that two paths are aliases.

The fixture was tested on macOS with installed Python 3.11.6, 3.13.2 and 3.14.3. These observations do not establish behavior for every network filesystem, link configuration, or Windows installation. Run the fixture in the deployment environment if its storage semantics are central to the failure.

Decide whether a same-file operation is a successful no-op

For an application-owned, stable directory, copying an already-identical file may reasonably mean “nothing to do.” Make that result visible to the caller. Do not report that new bytes were written. A backup workflow requiring an independent second copy must instead select a different destination and verify that it is independent.

The next example handles only the specific identity exception. It deliberately leaves other failures visible. It also checks the expected skip and copy outcomes on disposable files.

from pathlib import Path
import shutil
import tempfile


def copy_or_skip_same(source, destination):
    try:
        shutil.copyfile(source, destination)
    except shutil.SameFileError:
        return "skipped_same_file"
    return "copied"


with tempfile.TemporaryDirectory() as folder:
    source = Path(folder) / "input.txt"
    destination = Path(folder) / "output.txt"
    source.write_bytes(b"keep me\n")
    assert copy_or_skip_same(source, source) == "skipped_same_file"
    assert source.read_bytes() == b"keep me\n"
    assert copy_or_skip_same(source, destination) == "copied"
    assert destination.read_bytes() == source.read_bytes()
    try:
        copy_or_skip_same(source, Path(folder) / "absent" / "out.txt")
    except FileNotFoundError:
        print("unrelated failure remains visible")
    else:
        raise AssertionError("missing parent must fail")

This is an explicit application policy, not a universal replacement for copyfile. Do not catch every OSError and return the same skip status: missing sources, unreadable paths and failed writes need their own failure handling. A failed copy to a distinct destination is not guaranteed to leave that destination unchanged; use a separate transactional replacement design when that guarantee is required.

Treat identity checks as diagnostics in mutable directories

samefile can raise when a path cannot be inspected, including a missing destination. A missing destination is not an identity match, but the application still needs to handle creation failure. Permission or storage errors should remain distinguishable from absence.

A path can also change between an identity check and a subsequent copy. Adding if not samefile(...) does not lock the directory or prevent replacement by another process. For hostile or concurrently modified locations, define the required filesystem and handle-based guarantees before designing the operation. The two small examples here assume owned, stable fixtures and do not claim those guarantees.

Verify the result your workflow actually requires

For a no-op policy, verify the original bytes remain and return a distinct skip outcome. For an independent copy, verify content and that the resulting destination is a separate file. For a no-overwrite policy, use the separate exclusive-create copy guide rather than assuming that the absence of SameFileError protects an existing destination. Check that linked guide's destination contract before adapting its code.

Keep identity collisions, content equality, overwrite policy and metadata preservation as separate decisions. That makes an incident report useful: it can say which paths were compared, which outcome the application chose, and which guarantees were actually checked.

Sources & further reading