Path.copy: Copy Files and Directories Safely

Published on: October 12, 2026
Reading time: 5 minutes
Computer with code representing file copying with Python Path.copy

Path.copy() extends the object-oriented pathlib workflow to file and directory copying. Instead of converting paths to strings and moving between unrelated APIs, applications can keep navigation, validation, and copying around Path objects. This is useful for build systems, backups, installers, data migrations, test fixtures, and content-processing pipelines.

This guide focuses on production concerns: destination conflicts, symbolic links, metadata, permissions, partial failures, compatibility, performance, and verification. A convenient method does not make copying automatically safe; it makes the operation easier to express.

What Path.copy does

The source is the current path object and the destination is supplied to the method. A file copy reproduces its contents at the destination. A directory copy may replicate an entire tree, depending on the supported options and Python version. The returned path can be used for logging, validation, or later operations.

Before copying, define the contract. Decide whether an existing destination may be replaced, whether links are followed or preserved, which metadata matters, and what should happen after a partial failure.

Basic example

from pathlib import Path

source = Path("data/report.csv")
destination = Path("backup/report.csv")

destination.parent.mkdir(parents=True, exist_ok=True)
result = source.copy(destination)
print(result)

Create the parent directory explicitly. This makes behavior easier to review and avoids depending on assumptions. Keep the returned path when the next step needs to inspect size, permissions, or checksums.

Copying directory trees

A recursive copy is normally not an atomic transaction. If one item fails, part of the destination tree may already exist. For consistency-sensitive workflows, copy to a temporary sibling directory, verify it, and rename it into place.

from pathlib import Path
import tempfile

source = Path("site")
destination = Path("dist/site")

with tempfile.TemporaryDirectory(dir=destination.parent) as tmp:
    staged = Path(tmp) / "site"
    source.copy(staged)
    if destination.exists():
        raise FileExistsError(destination)
    staged.replace(destination)

The final rename is often much safer than exposing an incomplete directory as the official result. Verify filesystem semantics when the temporary location and destination are not on the same volume.

Overwrite policy

Never assume that an existing destination will be replaced. Read the documentation for the deployed Python version and implement an explicit policy. Backup tools should usually create versioned names. Reproducible build systems may remove their own previous output. User documents should not be overwritten silently.

def copy_without_replacing(source: Path, destination: Path) -> Path:
    if destination.exists():
        raise FileExistsError(f"Destination exists: {destination}")
    destination.parent.mkdir(parents=True, exist_ok=True)
    return source.copy(destination)

In concurrent systems, a check followed by a copy can still race with another process. Use unique staging directories, locks, or randomly generated destination names when collisions matter.

Symbolic links require a deliberate security decision. Following a link copies the target. Preserving it copies the reference. When input is not trusted, following links may escape the expected source tree and expose sensitive files. Resolve paths carefully and enforce an allowed root.

root = Path("incoming").resolve()
source = Path("incoming/item").resolve()
if source != root and root not in source.parents:
    raise ValueError("Source escapes the allowed root")

This check improves safety but does not remove all race conditions. A privileged service should avoid processing attacker-controlled mutable directory trees without stronger operating-system protections.

Metadata and permissions

File contents, timestamps, mode bits, ownership, ACLs, and extended attributes are separate concerns. A copy may preserve some and drop others, especially across different filesystems. If metadata fidelity is a requirement, test it on every supported platform and validate the exact attributes after copying.

Apply least privilege to the destination. Sensitive configuration or credential files should not become broadly readable because of inherited directory defaults. Adjust permissions after the copy and avoid logging confidential file contents.

Error handling

Handle specific exceptions such as FileNotFoundError, PermissionError, FileExistsError, and OSError. Record the source, destination, and operation stage, while redacting sensitive path details from user-facing messages.

try:
    source.copy(destination)
except FileNotFoundError:
    print("The source does not exist")
except PermissionError:
    print("Permission denied")
except OSError as error:
    print(f"Filesystem failure: {error}")

For batches, decide whether one error stops the entire job. A robust tool usually reports successful and failed items separately and leaves enough information for a safe retry.

Performance

Copy speed depends on storage latency, file size, caches, network behavior, and operating-system support. Do not read an entire file into Python memory just to duplicate it. Measure realistic workloads, check free space before large jobs, and limit concurrency so multiple copies do not destroy throughput.

Remote destinations need special care. Retry only transient failures, apply backoff, and detect partially written results before repeating an operation. Blind retries can create duplicate or inconsistent data.

Verification

For ordinary files, existence and size may be enough. For critical artifacts, compare cryptographic hashes. Hashing adds I/O but detects corruption and incomplete copies.

import hashlib

def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for block in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()

For directory trees, compare expected relative paths and optionally hash selected or all files. Store verification results in structured logs.

Version compatibility

Confirm the minimum Python version before relying on Path.copy. Libraries that support older versions can centralize a fallback through shutil.

def copy_path(source: Path, destination: Path) -> Path:
    if hasattr(source, "copy"):
        return source.copy(destination)
    import shutil
    if source.is_dir():
        return Path(shutil.copytree(source, destination))
    return Path(shutil.copy2(source, destination))

Centralizing compatibility avoids scattered version checks and creates one place for overwrite, symlink, logging, and metadata policy.

Testing strategy

Test a small file, empty file, nested directory, existing destination, missing permissions, symbolic links, Unicode names, long paths, full disks, and interruption during a recursive copy. Use tempfile.TemporaryDirectory for isolated tests and run integration tests on all supported operating systems.

Practical design

A reusable copy service should validate the source, enforce an allowed root, create destination parents, define overwrite behavior, control symbolic links, use staging for directory trees, check capacity, verify important outputs, and return a structured result. It should never accept arbitrary user paths without normalization and policy checks.

Related Academify guides include Python pathlib, Python shutil, Python files, and exception handling. See the official pathlib documentation and the official shutil documentation.

Conclusion

Path.copy makes copying fit naturally into modern pathlib code. Its concise interface should be combined with explicit overwrite rules, link handling, metadata expectations, staged directory updates, verification, and compatibility tests. With those controls, it can simplify filesystem workflows without hiding their operational risks.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Code on a monitor representing native memory access with Python ctypes.memoryview_at
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ctypes.memoryview_at: Zero-Copy Native Memory

    Learn Python ctypes.memoryview_at for zero-copy native memory access, pointer validation, ownership, and safer C integration.

    Ler mais

    Tempo de leitura: 6 minutos
    12/10/2026
    Python code on screen representing EnumDict and advanced enumerations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    EnumDict: Build Custom Enums with Validation

    Learn Python EnumDict for custom EnumType metaclasses, member validation, and safer advanced enumerations.

    Ler mais

    Tempo de leitura: 4 minutos
    11/10/2026
    Server rack representing connection load balancing with Python SO_REUSEPORT_LB
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    SO_REUSEPORT_LB: Balance Connections Across Workers

    Learn Python SO_REUSEPORT_LB to distribute connections across multiple workers with safe testing, portability, and graceful shutdown.

    Ler mais

    Tempo de leitura: 5 minutos
    11/10/2026
    Python asynchronous code on a laptop for inspect.markcoroutinefunction
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    markcoroutinefunction: Detect Async Wrappers

    Learn inspect.markcoroutinefunction in Python to identify async wrappers, integrate frameworks, and avoid incorrect coroutine detection.

    Ler mais

    Tempo de leitura: 5 minutos
    10/10/2026
    Python code for traversing folders and files with Path.walk
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Path.walk: Traverse Directories Safely

    Learn Python Path.walk to traverse directories, filter files, handle errors, and control directory-tree processing safely.

    Ler mais

    Tempo de leitura: 5 minutos
    10/10/2026
    Python process debugging in a terminal with source code
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pdb -p: Debug Running Python Processes

    Learn how to attach pdb to a running Python process, inspect stacks, and diagnose hangs safely.

    Ler mais

    Tempo de leitura: 6 minutos
    09/10/2026