- Python
Pythonファイルのコピー・移動・削除|shutil早見表とエラー対処
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期連続の黒字決算。