Hands-on shutil.copyfile vs manual file copy lab: real MB/s versus open/read/write and Path.read_bytes on local disk, measured on Linux localhost (lab).
4 min read
Copying file bytes on disk: is shutil.copyfile faster than a manual open + read/write loop (or Path.read_bytes / write_bytes)? This lab measures MB/s on Linux localhost temp files — not another pathlib-vs-os.path join lab (that is already published).
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Same filesystem tempdir. Affiliates: 0. copyfile copies data only; copy2 also copies metadata.
Verdict up front: 1 MiB — copyfile ~1823 MB/s vs manual 64 KiB chunks ~1542 (~1.18×) vs Path read/write ~1557. 8 MiB — copyfile ~1346 vs read-all ~879 (~1.53×); chunked manual stayed close. Prefer shutil.copyfile (or copy2 when you need mtime/mode); avoid slurping large files into RAM.
| Arm | Pattern |
|---|---|
copyfile | shutil.copyfile(src, dst) |
copy2 | data + metadata |
| manual chunk 64 KiB | read/write loop |
| manual readall | read() then write() |
| Path read/write | dst.write_bytes(src.read_bytes()) |
Sizes: 4 KiB / 64 KiB / 1 MiB / 8 MiB with repeated copies.
Script: lab-evidence/74-shutil-copyfile-vs-manual/results/run_lab.py.
| Arm | 4 KiB | 64 KiB | 1 MiB | 8 MiB |
|---|---|---|---|---|
| copyfile | 61 | 634 | 1823 | 1346 |
| copy2 | 54 | 551 | 1738 | 1931 |
| manual chunk | 72 | 730 | 1542 | 1420 |
| manual readall | 73 | 733 | 1549 | 879 |
| Path r/w | 69 | 722 | 1557 | 1394 |
| Arm | copies/s |
|---|---|
| manual readall | 18,724 |
| manual chunk | 18,550 |
| Path r/w | 17,637 |
| copyfile | 15,720 |
| copy2 | 13,845 |
Tiny files are dominated by open/unlink overhead — manual can look “faster” in a tight loop.
copyfile edged chunked manual by ~1.18× here (C helper, fewer Python trips).read() entire large files — at 8 MiB readall trailed copyfile by ~1.53×.copy2 costs metadata on small files; on larger buffered copies it can still win on this box (cache/FS noise — treat as “same league”).Lab 47 already covered Path vs os.path for join/exists/stat. This lab answers a different question: how you move bytes once you have two paths.
Need only bytes? copyfile. Need the destination to look like the source in ls -l timestamps/mode? copy2. Do not invent a third wrapper that stats and chmods by hand unless you are filtering metadata on purpose.
copy2 when you only need bytes — extra syscalls on small files.| Need | Prefer |
|---|---|
| Data-only copy | shutil.copyfile |
| Preserve mtime/mode | shutil.copy2 |
| Custom transform while copying | manual chunked loop |
| Tiny script convenience | Path read/write |
Evidence: /workspace/lab-evidence/74-shutil-copyfile-vs-manual/results/.
Use shutil for whole-file copies; chunk if you must DIY. On this box 1 MiB copyfile hit ~1823 MB/s (~1.18× chunked manual), and at 8 MiB it beat full-file readall by ~1.53×. Reach for pathlib when paths are the story — for bytes, copyfile is the default.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5. Same filesystem tempdir. Measured 4 KiB, 64 KiB, 1 MiB, and 8 MiB repeated copies. At 1 MiB copyfile reached ~1823 MB/s vs manual chunk ~1542; at 8 MiB copyfile ~1346 vs readall ~879. Affiliates: 0. Evidence: lab-evidence/74-shutil-copyfile-vs-manual/results/.
Hands-on fnmatch.filter vs re.compile name-list filtering measured on Linux localhost.
Lab1 Oct 20264 min read
Hands-on tarfile vs zipfile create+extract lab: real MB/s on a mixed small-file fixture (uncompressed tar vs zip), measured on Linux localhost for SREs.
Lab1 Oct 20264 min read
Hands-on os.scandir vs listdir vs Path.iterdir lab: real entries/s for names and is_file on a synthetic tree, measured on Linux localhost (lab) for SREs.
Lab1 Oct 20264 min read