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







