Pythonでファイルをコピー・移動・削除するには、標準ライブラリの shutil(コピー・移動・フォルダごと削除)と os/pathlib(1ファイルの削除・名前変更)を組み合わせます。 コピーは shutil.copy2()、フォルダごとのコピーは shutil.copytree()、移動は shutil.move()、ファイル削除は os.remove() か Path.unlink()、フォルダごとの削除は shutil.rmtree() が基本の5つです。

この記事では、似た名前の関数12個を 「対象・メタデータ保持・上書き挙動・戻り値」の4つの軸で一覧表にし、すべて Python 3.14.7(Linux)で実際に動かした出力を載せています。FileExistsError や OSError: Directory not empty などのエラーも実際のトレースバックで再現し、原因と直し方を対にしました。Python 3.14 で加わった Path.copy() / Path.move() の実行結果もあります。

ファイルのコピー・移動・削除の早見表

まず「やりたいこと」から関数を選ぶ表です。

やりたいこと 書き方 ポイント
ファイルをコピーする shutil.copy2(src, dst) 更新時刻も引き継ぐ。迷ったらこれ
更新時刻を「今」にしてコピーする shutil.copy(src, dst) 内容と権限だけをコピー
フォルダごとコピーする shutil.copytree(src, dst) コピー先が既にあると FileExistsError
既存フォルダへ上書きマージする shutil.copytree(src, dst, dirs_exist_ok=True) 3.8 以降
一部を除外してコピーする shutil.copytree(src, dst, ignore=shutil.ignore_patterns("*.tmp")) ワイルドカードで指定
ファイル・フォルダを移動する shutil.move(src, dst) dst が既存フォルダなら「その中」に入る
名前を変える(上書きあり) os.replace(src, dst) OS を問わず上書きする
ファイルを1つ削除する os.remove(path) / Path(path).unlink() フォルダには使えない
なくてもエラーにせず削除する Path(path).unlink(missing_ok=True) 3.8 以降
空のフォルダを削除する os.rmdir(path) / Path(path).rmdir() 中身があると OSError
中身ごとフォルダを削除する shutil.rmtree(path) 元に戻せないので対象を確認してから
ごみ箱へ移す send2trash.send2trash(path) 外部ライブラリ(後述)

次に、12個の関数を同じ軸で比べた表です。「上書き挙動」と「戻り値」は下の実行結果で確かめたものです。

関数 対象 メタデータ保持 コピー先・移動先が既にあるとき 戻り値
shutil.copy ファイル 権限のみ ファイルなら上書き。フォルダならその中に同名で作成 作成先のパス(文字列)
shutil.copy2 ファイル 権限+更新時刻など copy と同じ 作成先のパス
shutil.copyfile ファイル なし(内容のみ) ファイルなら上書き。フォルダを渡すと IsADirectoryError dst
shutil.copytree フォルダ あり(既定で copy2 を使用) FileExistsError。dirs_exist_ok=True でマージ dst
shutil.move ファイル・フォルダ あり ファイルなら上書き。フォルダならその中へ。中に同名があると shutil.Error 移動先のパス
shutil.rmtree フォルダ(中身ごと) ― ― None
os.remove ファイル ― ― None
os.rmdir 空のフォルダ ― ― None
os.rename ファイル・フォルダ あり(同じ実体) Linux・macOS はファイルを上書き。Windows は FileExistsError None
os.replace ファイル・フォルダ あり(同じ実体) どの OS でもファイルを上書き None
Path.unlink ファイル ― ― None
Path.rename ファイル・フォルダ あり(同じ実体) os.rename と同じ 新しい Path

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Pythonファイルのコピー・移動・削除|shutil早見表とエラー対処」

この記事の検証環境と準備

  • Python 3.14.7(Linux)。権限エラーの再現だけは、一般ユーザーで Python 3.11.15 を使いました
  • エラーメッセージは Linux のものです。Windows では [WinError 5] アクセスが拒否されました のように文言が変わります(Windows での出力は未実行です)
  • トレースバック中のファイルパスは、読みやすいように /home/user/demo/ へ短く書き換えています。それ以外の行は出力のままです

サンプルで使う元ファイルは次のコードで作ります。更新時刻をわざと 2024年1月15日にしておき、コピー後に時刻がどうなるかを見ます。

import os
from datetime import datetime
from pathlib import Path

Path("src.txt").write_text("hello\n")
old = datetime(2024, 1, 15, 9, 0, 0).timestamp()
os.utime("src.txt", (old, old))      # 更新時刻を 2024-01-15 に設定
os.chmod("src.txt", 0o640)           # 権限を 640 に設定

ファイルをコピーする(copy・copy2・copyfile の違い)

3つの違いは「何まで引き継ぐか」です。 同じファイルを3通りでコピーして、更新日と権限を並べます。

import os
import shutil
from datetime import datetime

r1 = shutil.copy("src.txt", "a_copy.txt")
r2 = shutil.copy2("src.txt", "a_copy2.txt")
r3 = shutil.copyfile("src.txt", "a_copyfile.txt")
print(repr(r1), repr(r2), repr(r3))

for f in ["src.txt", "a_copy.txt", "a_copy2.txt", "a_copyfile.txt"]:
    st = os.stat(f)
    day = datetime.fromtimestamp(st.st_mtime).strftime("%Y-%m-%d")
    print(f"{f:15} mtime={day} mode={oct(st.st_mode & 0o777)}")

実行結果(Python 3.14.7):

'a_copy.txt' 'a_copy2.txt' 'a_copyfile.txt'
src.txt         mtime=2024-01-15 mode=0o640
a_copy.txt      mtime=2026-10-02 mode=0o640
a_copy2.txt     mtime=2024-01-15 mode=0o640
a_copyfile.txt  mtime=2026-10-02 mode=0o644
関数 内容 権限(mode) 更新時刻 向いている場面
copyfile ○ ×(新規作成時の既定値 644) ×(コピーした時刻) 中身だけ写したい
copy ○ ○(640 のまま) ×(コピーした時刻) 「コピーした日」を残したい
copy2 ○ ○ ○(2024-01-15 のまま) バックアップ、日付で整理するファイル

copy2 は更新時刻が元のまま、copy はコピーした日時になります。 写真や帳票のように「いつ作られたか」が意味を持つファイルは copy2 を選ぶと、後で日付順に並べたときに順番が保たれます。なお、所有者や作成日時など、OS によっては引き継がれない情報もあります。

コピー先にフォルダを指定するとどうなるか

copy と copy2 は、コピー先が既存のフォルダなら、その中に同じ名前でコピーします。ファイル名まで書けば、その名前になります。

import os
import shutil

os.mkdir("backup")
print(repr(shutil.copy("src.txt", "backup")))              # フォルダを指定
print(repr(shutil.copy("src.txt", "backup/renamed.txt")))  # ファイル名まで指定
print(repr(shutil.copy("src.txt", "backup")))              # もう一度(上書き)
'backup/src.txt'
'backup/renamed.txt'
'backup/src.txt'

3行目のとおり、同名のファイルがあっても確認なしで上書きされます。 上書きを避けたいときは、後述の「同名があれば連番を付けてコピー」を使ってください。戻り値は実際に作られたパスなので、ログに残すと追いやすくなります。

フォルダごとコピーする(copytree)

shutil.copytree(src, dst) は、フォルダの中身を階層ごとコピーします。次の構成のフォルダ proj で試します。

import os
import shutil
from pathlib import Path

for d in ["proj/src", "proj/__pycache__", "proj/logs"]:
    os.makedirs(d)
for f in ["proj/src/main.py", "proj/__pycache__/main.cpython-314.pyc",
          "proj/logs/app.log", "proj/README.md", "proj/memo.tmp"]:
    Path(f).write_text("x")

print(repr(shutil.copytree("proj", "proj_bak")))
print(sorted(str(p) for p in Path("proj_bak").rglob("*")))
'proj_bak'
['proj_bak/README.md', 'proj_bak/__pycache__', 'proj_bak/__pycache__/main.cpython-314.pyc', 'proj_bak/logs', 'proj_bak/logs/app.log', 'proj_bak/memo.tmp', 'proj_bak/src', 'proj_bak/src/main.py']

既存フォルダへ上書きマージする(dirs_exist_ok)

コピー先が既にあると、copytree は FileExistsError になります。 2回目以降も同じ場所へコピーしたいときは dirs_exist_ok=True を付けます(Python 3.8 以降)。

import shutil
from pathlib import Path

Path("proj/new.txt").write_text("n")       # 元フォルダにファイルを追加
shutil.copytree("proj", "proj_bak", dirs_exist_ok=True)
print(Path("proj_bak/new.txt").exists())
True

dirs_exist_ok=True は「足りないものを足し、同名は上書きする」動きです。コピー先にだけあるファイルは消えずに残るので、完全に同じ状態にしたいときは、先に shutil.rmtree() で消してから copytree します。

不要なファイルを除外する(ignore_patterns)

ignore=shutil.ignore_patterns(...) に、除外したい名前をワイルドカードで渡します。

import shutil
from pathlib import Path

shutil.copytree(
    "proj", "proj_clean",
    ignore=shutil.ignore_patterns("__pycache__", "*.tmp", "logs"),
)
print(sorted(str(p) for p in Path("proj_clean").rglob("*")))
['proj_clean/README.md', 'proj_clean/new.txt', 'proj_clean/src', 'proj_clean/src/main.py']

__pycache__・logs フォルダと memo.tmp が除かれました。パターンはフォルダ名にもファイル名にも当てはまり、すべての階層に効きます。

copytree の引数 既定値 役割
dirs_exist_ok False True で既存フォルダへのコピーを許可
ignore None 除外する名前を返す関数。ignore_patterns() で作る
copy_function copy2 ファイル1つのコピー方法。shutil.copy に変えると時刻を引き継がない
symlinks False True でシンボリックリンクをリンクのままコピー

なお、copytree にファイルを渡すと NotADirectoryError: [Errno 20] Not a directory: 'src.txt' になります。ファイルは copy2、フォルダは copytree と使い分けます。

ファイル・フォルダを移動する(move)

shutil.move(src, dst) は、ファイルもフォルダも移動できます。移動先が「既存のフォルダ」かどうかで結果が変わるのが最大のポイントです。

import os
import shutil
from pathlib import Path

Path("m1.txt").write_text("1")
print(repr(shutil.move("m1.txt", "m1_renamed.txt")))   # ① 名前の変更
os.mkdir("dest")
print(repr(shutil.move("m1_renamed.txt", "dest")))     # ② 既存フォルダの中へ

os.mkdir("d_a"); Path("d_a/f.txt").write_text("f"); os.mkdir("d_b")
print(repr(shutil.move("d_a", "d_b")))                 # ③ フォルダを既存フォルダへ
os.mkdir("d_c"); Path("d_c/g.txt").write_text("g")
print(repr(shutil.move("d_c", "d_new")))               # ④ フォルダを存在しない名前へ
print(sorted(str(p) for p in Path("d_b").rglob("*")), sorted(str(p) for p in Path("d_new").rglob("*")))
'm1_renamed.txt'
'dest/m1_renamed.txt'
'd_b/d_a'
'd_new'
['d_b/d_a', 'd_b/d_a/f.txt'] ['d_new/g.txt']
移動先 dst の状態 結果 実行で確認した戻り値
存在しない その名前に変わる(リネーム) 'm1_renamed.txt' / 'd_new'
既存のフォルダ フォルダの中に入る 'dest/m1_renamed.txt' / 'd_b/d_a'
既存フォルダの中に同名がある shutil.Error 下記のトレースバック
既存のファイル(パスをファイル名まで指定) 上書き 'dest/m1_renamed.txt'

③のように、フォルダを既存フォルダへ移動すると「置き換え」ではなく「中に入る」ため、d_b/d_a/f.txt という入れ子になります。「d_a を d_b という名前にしたい」ときは、移動先が存在しない状態で実行します。

移動先フォルダに同名のファイルがあるときは、次のエラーになります。

Traceback (most recent call last):
  File "/home/user/demo/e_move.py", line 3, in <module>
    shutil.move("memo.txt", "backup")
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.14/shutil.py", line 920, in move
    raise Error("Destination path '%s' already exists" % real_dst)
shutil.Error: Destination path 'backup/memo.txt' already exists

上書きして移動したいときは、移動先をファイル名まで書きます。 shutil.move("memo.txt", "backup/memo.txt") とすると上書きされることを確認しました。

名前を変える(os.rename・os.replace・Path.rename)

同じドライブ内での名前変更だけなら、os.rename / os.replace も使えます。

import os
from pathlib import Path

Path("r1.txt").write_text("new"); Path("r2.txt").write_text("old")
print(os.replace("r1.txt", "r2.txt"), Path("r2.txt").read_text())
print(repr(Path("r2.txt").rename("r3.txt")), repr(Path("r3.txt").replace("r4.txt")))
None new
PosixPath('r3.txt') PosixPath('r4.txt')
関数 移動先が既存ファイルのとき 別ドライブへの移動 戻り値
os.rename Linux・macOS は上書き(実行で確認)。Windows は FileExistsError(未実行・一般に知られた仕様) できない None
os.replace どの OS でも上書き できない None
Path.rename / Path.replace 上の2つと同じ できない 新しい Path
shutil.move ファイル名まで指定すれば上書き できる(コピーして元を削除) 移動先のパス

OS によって結果を変えたくないときは os.replace、ドライブをまたぐ可能性があるときは shutil.move を選びます。

ファイル・フォルダを削除する(os.remove・rmdir・rmtree)

削除は「何を消すか」で関数が決まります。

消したいもの 関数 対象が違うときのエラー(実行で確認)
ファイル1つ os.remove(p) / Path(p).unlink() フォルダを渡すと IsADirectoryError
空のフォルダ os.rmdir(p) / Path(p).rmdir() 中身があると OSError: [Errno 39] Directory not empty、ファイルを渡すと NotADirectoryError
フォルダと中身すべて shutil.rmtree(p) ファイルを渡すと NotADirectoryError、なければ FileNotFoundError
import os
import shutil
from pathlib import Path

Path("del1.txt").write_text("x")
print(os.remove("del1.txt"))                       # ファイルを削除
print(Path("del1.txt").unlink(missing_ok=True))    # もう無いがエラーにならない
os.makedirs("e_dir/sub"); Path("e_dir/sub/x.txt").write_text("x")
print(shutil.rmtree("e_dir"), Path("e_dir").exists())
print(shutil.rmtree("nodir", ignore_errors=True))  # 無くてもエラーにしない
None
None
None False
None

os.remove と os.unlink は同じ働きです。削除系の関数はすべて None を返し、ごみ箱を経由せずに消えます。

よくあるエラーと直し方(実際のトレースバック)

ファイル操作のエラーは、最後の1行に 「例外名: [Errno 番号] 理由: 'パス'」 が出ます。パスの部分を見ると、どのファイルで起きたかがわかります。

エラー 起きる場面 直し方
FileExistsError copytree のコピー先や os.mkdir の対象が既にある dirs_exist_ok=True / exist_ok=True
FileNotFoundError コピー元がない、コピー先の親フォルダがない、削除対象がない 親フォルダを mkdir(parents=True, exist_ok=True)、missing_ok=True
PermissionError 書き込み権限のない場所、他のアプリが開いているファイル 権限・場所を見直す、ファイルを閉じる
IsADirectoryError ファイル用の関数にフォルダを渡した フォルダは copytree / rmtree
OSError: Directory not empty os.rmdir や os.rename の対象フォルダに中身がある shutil.rmtree / shutil.move
shutil.SameFileError コピー元とコピー先が同じファイル パスを確認する
shutil.Error: ... already exists move の移動先フォルダに同名がある 移動先をファイル名まで指定

FileExistsError: コピー先のフォルダが既にある

import shutil
shutil.copytree("reports", "backup")
Traceback (most recent call last):
  File "/home/user/demo/e_exists.py", line 2, in <module>
    shutil.copytree("reports", "backup")
    ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  (中略:shutil.py 内の copytree → _copytree → os.makedirs)
FileExistsError: [Errno 17] File exists: 'backup'

直し方: 既存フォルダへ重ねてよいなら shutil.copytree("reports", "backup", dirs_exist_ok=True)。毎回まっさらにしたいなら、日付入りの新しいフォルダ名(backup_20261002 など)にする方法もあります。

FileNotFoundError: コピー先の親フォルダがない

import shutil
shutil.copy("memo.txt", "archive/2026/memo.txt")
Traceback (most recent call last):
  File "/home/user/demo/e_notfound.py", line 2, in <module>
    shutil.copy("memo.txt", "archive/2026/memo.txt")
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.14/shutil.py", line 489, in copy
    copyfile(src, dst, follow_symlinks=follow_symlinks)
    ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.14/shutil.py", line 315, in copyfile
    with open(dst, 'wb') as fdst:
         ~~~~^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'archive/2026/memo.txt'

コピー元 memo.txt は存在しています。メッセージに出ているパスがコピー先である点が手がかりで、archive/2026 フォルダがないことが原因です。shutil.copy は親フォルダを自動では作りません。

直し方: コピーの前に親フォルダを作ります。

from pathlib import Path
import shutil

dst = Path("archive/2026/memo.txt")
dst.parent.mkdir(parents=True, exist_ok=True)   # 途中のフォルダもまとめて作成
shutil.copy2("memo.txt", dst)

コピー元がない場合は FileNotFoundError: [Errno 2] No such file or directory: 'none.txt' のように、コピー元のパスが表示されます。この場合は、実行しているフォルダ(カレントディレクトリ)が想定どおりかを Path.cwd() で確かめると早く解決できます。

PermissionError: 書き込み権限がない

書き込み権限のないフォルダ locked の中のファイルを、一般ユーザーで削除した結果です(Python 3.11.15)。

Traceback (most recent call last):
  File "/home/user/demo/perm.py", line 13, in <module>
    try: os.remove("locked/report.txt")
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
PermissionError: [Errno 13] Permission denied: 'locked/report.txt'

同じ環境で、関連する操作も試しました。

操作 結果
書き込み不可のフォルダ内のファイルを os.remove PermissionError: [Errno 13] Permission denied: 'locked/report.txt'
書き込み不可のフォルダへ shutil.copy PermissionError: [Errno 13] Permission denied: 'locked/new.txt'
書き込み不可のフォルダを shutil.rmtree PermissionError: [Errno 13] Permission denied: 'report.txt'
読み取り専用ファイルへ shutil.copyfile で上書き PermissionError: [Errno 13] Permission denied: 'open/readonly.txt'
書き込み可能なフォルダ内の読み取り専用ファイルを os.remove 削除できた(None)

最後の行のとおり、Linux・macOS では「ファイルを消せるかどうか」は、ファイル自身ではなく、入っているフォルダの書き込み権限で決まります。

直し方の確認リスト:

  • 対象のパスが、自分のユーザーで書き込める場所か(os.access(path, os.W_OK) で確認できます)
  • Excel やエディタなど、ほかのアプリでそのファイルを開いていないか(Windows では開いているファイルの削除・移動が PermissionError になることが知られています。今回は未実行です)
  • ファイル用の関数にフォルダを渡していないか(Windows では os.remove にフォルダを渡した場合も PermissionError になることが知られています。未実行です)

IsADirectoryError: ファイル用の関数にフォルダを渡した

import os
os.remove("reports")
Traceback (most recent call last):
  File "/home/user/demo/e_isdir.py", line 2, in <module>
    os.remove("reports")
    ~~~~~~~~~^^^^^^^^^^^
IsADirectoryError: [Errno 21] Is a directory: 'reports'

shutil.copyfile("memo.txt", "backup") のように、copyfile のコピー先にフォルダを渡したときも IsADirectoryError: [Errno 21] Is a directory: 'backup' になります。

直し方: フォルダを消すなら shutil.rmtree("reports")、フォルダへコピーするなら shutil.copy / copy2 を使います。ファイルとフォルダが混ざるときは Path(p).is_dir() で分けます。

OSError: Directory not empty(フォルダが空でない)

import os
os.rmdir("reports")
Traceback (most recent call last):
  File "/home/user/demo/e_notempty.py", line 2, in <module>
    os.rmdir("reports")
    ~~~~~~~~^^^^^^^^^^^
OSError: [Errno 39] Directory not empty: 'reports'

os.rmdir は空のフォルダ専用です。中身ごと消すなら shutil.rmtree("reports") を使います。このエラーは専用の例外クラスがなく OSError として届くので、種類を見分けたいときは errno を見ます。

import errno
import os

try:
    os.rmdir("reports")
except OSError as e:
    if e.errno == errno.ENOTEMPTY:
        print("中身があるので残しました:", e.filename)
    else:
        raise

「空なら消す、中身があれば残す」という片付けに、そのまま使える形です。os.rename("e_src", "e_dir") で移動先フォルダに中身があるときも、同じく OSError: [Errno 39] Directory not empty: 'e_src' -> 'e_dir' になることを確認しました。

例外をまとめて受け取る書き方

ここまでの例外は、すべて OSError の仲間です(shutil.Error と shutil.SameFileError も OSError を継承しています)。

import shutil

try:
    shutil.copy2("memo.txt", "archive/2026/memo.txt")
except FileNotFoundError as e:
    print("見つかりません:", e.filename)
except PermissionError as e:
    print("権限がありません:", e.filename)
except OSError as e:
    print("そのほかのエラー:", e)

細かい例外を先に、OSError を最後に書くと、原因ごとにメッセージを分けられます。

安全に削除する方法

削除は元に戻せないので、「消す前に確かめる仕組み」をコードに入れておくと安心です。

なくてもエラーにしない(missing_ok・例外で受ける)

書き方 特徴
Path(p).unlink(missing_ok=True) いちばん短い(3.8 以降)
try: os.remove(p) / except FileNotFoundError: pass どのバージョンでも使える
with contextlib.suppress(FileNotFoundError): os.remove(p) 1行にまとめたいとき
if os.path.exists(p): os.remove(p) 読みやすいが、確認と削除の間に別の処理が消す可能性が残る
shutil.rmtree(p, ignore_errors=True) フォルダ用。すべてのエラーを無視する点に注意

os.path.exists() での事前確認は読みやすい一方、確認してから削除するまでのわずかな間に状態が変わることがあります。「実行してみて、なければ例外で受ける」書き方のほうが、複数の処理が同時に動く場面でも結果が安定します。

決めたフォルダの外は消さない関数

shutil.rmtree に渡すパスが変数のとき、空文字や想定外の値が入ると、広い範囲を消してしまうことがあります。「このフォルダの内側だけ」と範囲を決める関数を1つ用意しておくと安全です。

from pathlib import Path
import shutil

def safe_delete(target, base_dir):
    """base_dir の内側にあるファイル・フォルダだけを削除する"""
    base = Path(base_dir).resolve()
    p = Path(target).resolve()
    if p == base or base not in p.parents:
        raise ValueError(f"削除対象が {base} の外です: {p}")
    if not p.exists():
        return "なし(何もしない)"
    if p.is_dir() and not p.is_symlink():
        shutil.rmtree(p)
        return "フォルダを削除"
    p.unlink()
    return "ファイルを削除"

Path("tmp_area/cache").mkdir(parents=True, exist_ok=True)
Path("tmp_area/cache/a.txt").write_text("a")
Path("tmp_area/old.log").write_text("log")

print(safe_delete("tmp_area/old.log", "tmp_area"))
print(safe_delete("tmp_area/cache", "tmp_area"))
print(safe_delete("tmp_area/none.txt", "tmp_area"))
try:
    safe_delete("tmp_area/../src.txt", "tmp_area")
except ValueError as e:
    print("ValueError:", e)

実行結果(絶対パスの前半は ... に省略):

ファイルを削除
フォルダを削除
なし(何もしない)
ValueError: 削除対象が .../tmp_area の外です: .../src.txt

resolve() で .. を解決してから比べているので、tmp_area/../src.txt のような指定も範囲外として止められます。

ごみ箱へ移す(send2trash)

標準ライブラリの削除は、ごみ箱を経由しません。「あとで戻せるようにしたい」ときは、外部ライブラリの Send2Trash が使えます。

pip install Send2Trash
from send2trash import send2trash

send2trash("old_report.txt")   # OS のごみ箱へ移動

PyPI で 2.1.0 が公開されていることは pip で確認しました。ごみ箱は画面のある OS の機能なので、今回の検証環境(画面のない Linux)では動かしておらず、上のコードは未実行の紹介です。手元の PC で整理スクリプトを動かすときの選択肢として覚えておくと便利です。

削除前のチェックリスト

確認すること 方法
対象が想定どおりか 先に print() で一覧を出す「お試し実行」を用意する
パスが空や / になっていないか safe_delete のように基準フォルダと比べる
相対パスの基準は正しいか Path.cwd() を表示する。スクリプト基準なら Path(__file__).parent
戻せる必要があるか 先に copy2 / make_archive で退避、または Send2Trash

そのまま使えるコード集

同名があれば連番を付けてコピーする

from pathlib import Path
import shutil

def copy_no_overwrite(src, dst_dir):
    """同名があれば _1, _2 ... を付けてコピーする"""
    src, dst_dir = Path(src), Path(dst_dir)
    dst_dir.mkdir(parents=True, exist_ok=True)
    dst = dst_dir / src.name
    n = 1
    while dst.exists():
        dst = dst_dir / f"{src.stem}_{n}{src.suffix}"
        n += 1
    return shutil.copy2(src, dst)

for _ in range(3):
    print(copy_no_overwrite("src.txt", "archive"))
archive/src.txt
archive/src_1.txt
archive/src_2.txt

拡張子ごとにフォルダへ振り分ける

from pathlib import Path
import shutil

inbox = Path("inbox")
inbox.mkdir(exist_ok=True)
for name in ["a.csv", "b.csv", "c.png", "d.txt", "e.PNG"]:
    (inbox / name).write_text("x")          # サンプルファイルを作成

rules = {".csv": "data", ".png": "images"}  # 拡張子 → フォルダ名
for f in sorted(inbox.iterdir()):
    if f.is_file():
        folder = inbox / rules.get(f.suffix.lower(), "others")
        folder.mkdir(exist_ok=True)
        print(shutil.move(f, folder / f.name))
inbox/data/a.csv
inbox/data/b.csv
inbox/images/c.png
inbox/others/d.txt
inbox/images/e.PNG

f.suffix.lower() で大文字の .PNG も同じフォルダへ入ります。移動先を folder / f.name とファイル名まで書いているので、同名があれば上書きになります。

フォルダを ZIP にまとめる(make_archive)

削除や上書きの前に退避しておきたいときは、shutil.make_archive() でフォルダを1つの ZIP にできます。

import shutil

path = shutil.make_archive("proj_archive", "zip", root_dir="proj_clean")
print(path)                                   # 作られた ZIP の絶対パスが返る
shutil.unpack_archive("proj_archive.zip", "unpacked")   # 展開

実行すると .../proj_archive.zip が作られ、unpacked フォルダに README.md・new.txt・src/main.py が展開されることを確認しました。第1引数には拡張子なしの名前を渡す点がポイントです。パスの組み立てやファイル一覧の取り方は、既存記事「Python pathlibの使い方」で詳しく扱っています。

Python 3.14 の Path.copy・Path.move

Python 3.14 から、pathlib の Path オブジェクトだけでコピーと移動ができるようになりました。公式ドキュメントで仕様を確認し、3.14.7 で実行しています。

メソッド 働き shutil での書き方
Path.copy(target) ファイルまたはフォルダを target へコピー copy / copytree
Path.copy_into(target_dir) 既存フォルダの中へコピー copy(src, dir)
Path.move(target) target へ移動 move
Path.move_into(target_dir) 既存フォルダの中へ移動 move(src, dir)
import os
from datetime import datetime
from pathlib import Path

def day(p):
    return datetime.fromtimestamp(p.stat().st_mtime).strftime("%Y-%m-%d")

p = Path("src.txt")                                   # 更新時刻は 2024-01-15
c1 = p.copy("p_copy.txt")
c2 = p.copy("p_copy_meta.txt", preserve_metadata=True)
print(repr(c1), day(c1), day(c2))

os.mkdir("p_dir")
print(repr(p.copy_into("p_dir")))                     # フォルダの中へコピー
print(repr(Path("proj_clean").copy("p_tree")))        # フォルダもコピーできる
print(repr(Path("p_copy.txt").move("p_moved.txt")))   # 移動(名前の変更)
print(repr(Path("p_moved.txt").move_into("p_dir")))   # フォルダの中へ移動
PosixPath('p_copy.txt') 2026-10-02 2024-01-15
PosixPath('p_dir/src.txt')
PosixPath('p_tree')
PosixPath('p_moved.txt')
PosixPath('p_dir/p_moved.txt')

shutil と比べたときの違いを、実行結果からまとめます。

観点 shutil 3.14 の Path メソッド
メタデータ copy2 は引き継ぐ、copy は権限のみ 既定では引き継がない。preserve_metadata=True で引き継ぐ(上の 2024-01-15)
移動先が既存フォルダ 自動で「中に入る」 move は中に入らない。入れたいときは move_into と明示
フォルダのコピー先が既にある FileExistsError 同じく FileExistsError: [Errno 17] File exists: 'p_tree'
移動先が中身のあるフォルダ shutil.Error など OSError: [Errno 39] Directory not empty: 'p_dir' -> 'proj'
戻り値 文字列(渡した型に準じる) 新しい Path

「中に入れる」のか「その名前にする」のかをメソッド名で書き分けられるので、shutil.move の「移動先がフォルダかどうかで結果が変わる」迷いがなくなります。3.13 以前でも動かす必要があるコードは、これまでどおり shutil を使います。

どの関数を選ぶかの判断表

状況 選ぶ関数 理由
バックアップを取りたい copy2 / copytree 更新時刻が残り、あとで新旧を比べられる
ひな形から新しいファイルを作りたい copy 更新時刻が「作った日」になる
何度も同じ場所へ同期したい copytree(..., dirs_exist_ok=True) 2回目以降もエラーにならない
名前を変えるだけ Path.rename / os.replace 同じドライブ内なら一瞬で終わる
別ドライブ・ネットワーク上へ移す shutil.move コピーしてから元を消す動きに自動で切り替わる
一時ファイルを片付ける Path.unlink(missing_ok=True) なくてもエラーにならない
作業用フォルダを丸ごと片付ける shutil.rmtree 中身ごと消せる。範囲の確認とセットで
3.14 以降だけで動けばよい Path.copy / Path.move_into など Path だけで書ける

よくある質問

Q. shutil.copy と shutil.copy2 はどちらを使えばよいですか? A. 迷ったら copy2 です。内容と権限に加えて更新時刻も引き継ぐので、バックアップや日付での整理に向いています。「コピーした日時」を更新時刻にしたいときだけ copy を選びます。

Q. shutil.move で上書きするにはどうすればよいですか? A. 移動先をフォルダではなくファイル名まで指定します。shutil.move("a.txt", "dest") は同名があると shutil.Error になりますが、shutil.move("a.txt", "dest/a.txt") は上書きされます。

Q. os.remove と os.unlink、Path.unlink の違いは何ですか? A. os.remove と os.unlink は同じ働きです。Path.unlink は Path オブジェクトのメソッド版で、missing_ok=True を付けると、ファイルがなくてもエラーになりません。どれもフォルダには使えません。

Q. shutil.rmtree で消したフォルダは元に戻せますか? A. ごみ箱を経由しないため、Python からは戻せません。戻せる余地を残したいときは、先に make_archive で ZIP に退避するか、Send2Trash でごみ箱へ移す方法があります。

まとめ

  • コピーは copy2(更新時刻も保持)、フォルダは copytree、移動は move、削除は os.remove/Path.unlink/shutil.rmtree が基本
  • copy は更新時刻がコピーした日時、copy2 は元のまま。実行結果で 2026-10-02 と 2024-01-15 に分かれた
  • copytree は既存のコピー先で FileExistsError。dirs_exist_ok=True でマージ、ignore_patterns で除外できる
  • shutil.move は移動先が既存フォルダなら「中に入る」。上書きしたいときはファイル名まで指定する
  • エラーは最後の1行のパスを見る。FileNotFoundError はコピー先の親フォルダがない場合にも出る
  • 削除は missing_ok=True、基準フォルダとの比較、事前の退避で安全にできる
  • Python 3.14 では Path.copy・copy_into・move・move_into が使え、「中に入れる/その名前にする」を書き分けられる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Pythonファイルのコピー・移動・削除|shutil早見表とエラー対処」

株式会社RJCでは、Pythonのファイル操作を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?