毎月の締め日に、決まったフォルダを別の場所へコピーして日付を付けて保存する。終わったら古いフォルダを消して、まとめてzipにして送る。
そんな手作業を、マウスで繰り返していませんか。
1回なら数分で終わります。ただ、これが毎月となると話は別です。しかも手作業には、コピー先を1つ間違えただけで取り返しがつかなくなる怖さがついて回ります。
こういう定型のファイル整理こそ、Pythonに任せてしまうのが向いています。そのための道具が、標準ライブラリのshutilです。
この記事では、shutilでよく使う関数と、初心者がつまずきやすいところを順番に見ていきます。
shutilは、ファイルを丸ごと動かすための道具¶
まずは立ち位置から確認しましょう。shutilは、ファイルやフォルダをコピーしたり、移動したり、まとめて消したりするための標準ライブラリです。
標準ライブラリなので、インストールは要りません。import shutil と書けば、その瞬間から使えます。
名前はshell utilities、つまりコマンド操作の便利道具という意味合いです。ターミナルで打つ cp や mv や rm を、Pythonのコードから呼べるようにしたもの、と考えるとしっくりきます。
似た役割のものにosモジュールがあります。両者の役割分担はおおむね次のとおりです。
| やりたいこと | 向いている道具 |
|---|---|
| ファイルの中身ごとコピーする | shutil |
| フォルダを中身ごと移動・削除する | shutil |
| zipやtarに固める | shutil |
| 1つのファイル名を変える | os.rename |
| 空のフォルダを作る・消す | os.makedirs / os.rmdir |
| パスを組み立てる・一覧を取る | pathlib |
ざっくり言えば、中身ごとまとめて動かしたいときがshutilの出番です。パスの組み立て自体は専用の道具に任せるほうが安全です。【関連記事】Pythonのpathlibとは?ファイルパス操作を初心者向けに解説
コピーの関数が4つもあるのはなぜか¶
shutilを開いて最初に戸惑うのが、コピー系の関数の多さです。copyfile、copy、copy2、copytree と並んでいて、どれを選べばいいのか迷います。
違いは、何をどこまで持っていくかにあります。運ぶ荷物の量が少しずつ違う、と考えてください。
| 関数 | 中身 | 権限(モード) | 更新日時 | コピー先にフォルダを指定 |
|---|---|---|---|---|
copyfile |
コピーする | しない | しない | できない |
copy |
コピーする | する | しない | できる |
copy2 |
コピーする | する | する | できる |
copytree |
フォルダごと | する | する | フォルダが対象 |
迷ったら copy2 を選んでおけば、たいていの場面で困りません。 ファイルの日付まで一緒に運んでくれるからです。
copyとcopy2の違いは、日付が残るかどうか¶
言葉で読むよりも、実際に動かしたほうが早いところです。同じファイルを2つの関数でコピーして、更新日時を比べてみましょう。
import os
import shutil
src = "report.xlsx"
a = shutil.copy(src, "backup/by_copy.xlsx") # 中身と権限
b = shutil.copy2(src, "backup/by_copy2.xlsx") # 中身と権限と日時
for path in (src, a, b):
print(path, os.path.getmtime(path))
手元で試すと、copy でコピーしたほうは更新日時が実行した瞬間の時刻に変わります。copy2 のほうは、元のファイルの日時がそのまま残ります。
この差は、思っているより実務に効きます。日付でファイルの新旧を判定する仕組みがあると、copy を使った途端に全部が最新扱いになってしまうからです。
私は10年ほどエンジニアとして開発に関わってきましたが、これで一度やられたことがあります。バックアップの取り込み処理を copy で書いてしまい、更新されていないファイルまで毎晩まるごと同期対象になって、転送量が10倍に膨れ上がりました。
原因にたどり着くまで半日かかりました。関数名が1文字違うだけの差が、ここまで響くのかと肝を冷やした一件です。
なお公式ドキュメントも、copy2 であっても作成者や所有者といったすべての情報を運べるわけではないと断っています。完全な複製ではなく、実用的な範囲での複製だと考えておきましょう。
copyfileはフォルダを受け取れない¶
もう1つ、初心者がよく踏むのが copyfile の落とし穴です。コピー先にフォルダを渡すと、IsADirectoryError で止まります。
copy や copy2 は、コピー先がフォルダなら同じ名前のまま中へ入れてくれます。copyfile は、あくまでファイル名からファイル名への複製しか受け付けません。
コピー先の書き方を変えるだけで直ります。ファイル名まで書くか、素直に copy2 へ乗り換えてください。
フォルダごとコピーするcopytree¶
ファイル1つではなく、フォルダの中身をまるごと運びたい場面もあります。そのための関数が copytree です。
サブフォルダも含めて、階層をそのまま再現してくれます。1ファイルずつのコピーには、既定で copy2 が使われます。
import shutil
shutil.copytree("project", "project_backup")
# 一部を除いてコピーする
shutil.copytree(
"project",
"project_backup2",
ignore=shutil.ignore_patterns("*.log", "__pycache__", ".venv"),
)
ignore_patterns に渡した名前は、コピーの対象から外れます。ログや仮想環境のフォルダを持っていく必要はまずないので、この指定はほぼ必須だと思っておいてください。
2回目に失敗するのは、dirs_exist_okを知らないから¶
copytree でいちばん多い質問が、これです。1回目はうまくいったのに、もう一度実行すると FileExistsError で止まる。
原因は、コピー先がすでに存在するからです。既定では、上書きを避けるためにエラーで止まる作りになっています。
上書きしてよいと分かっているなら、引数を1つ足すだけで解決します。dirs_exist_ok=True を指定してください。
shutil.copytree("project", "project_backup", dirs_exist_ok=True)
この引数はPython 3.8で追加されたものです。ネットのサンプルで見かけないときは、記事が3.8より前に書かれた可能性を疑ってみてください。
ただし、上書きは元に戻せません。コピー先が空でよいと確信できるときだけ使うのが安全です。
moveは、名前の変更にも使える¶
移動を担当するのが move です。ファイルにもフォルダにも同じように使えます。
振る舞いに、知っておくと得をする性質が2つあります。1つは、移動先が既存のフォルダなら、その中へ入るという点です。
import shutil
shutil.move("report.xlsx", "archive") # archive フォルダの中へ入る
shutil.move("report.xlsx", "archive/2026.xlsx") # 名前を変えて移動する
もう1つは、内部の動き方です。同じディスクの中での移動なら os.rename が使われるので一瞬で終わりますが、別のディスクやネットワーク越しだと、コピーしてから元を消す動きになります。
USBメモリやネットワークドライブへの移動が妙に遅いのは、これが理由です。大きなファイルを移すときは、途中で止めない心づもりでいてください。
rmtreeは、いちばん危ない関数¶
ここは真剣に読んでほしいところです。rmtree は、指定したフォルダを中身ごと消します。
ごみ箱には入りません。実行した瞬間に消えます。
import shutil
from pathlib import Path
target = Path("temp_output")
if target.is_dir(): # 存在確認をはさむ
shutil.rmtree(target)
if で存在を確かめているのは、無いフォルダを指定するとエラーになるからです。ただ、本当に大事なのは、消す対象が変数で組み立てられているときの怖さのほうです。
変数の中身が空文字だったら。パスの組み立てを1か所間違えていたら。想像したくない結果になります。
私は新人の頃、テスト用の一時フォルダを消すつもりで書いたコードを、設定ファイルの読み違いで別の場所に向けてしまったことがあります。幸い開発環境で気づけましたが、あの瞬間の血の気が引く感覚はいまも覚えています。
それ以来、削除するコードには必ず消す対象を表示する行を先に入れるようにしています。慣れた人ほど、この一手間を省きません。
エラーの扱いを細かく決めたいときは、引数も用意されています。読み取り専用のファイルがあって消せない、といった場面で使うものです。
| 引数 | 役割 |
|---|---|
ignore_errors=True |
消せないものがあっても止まらずに進む |
onexc |
失敗したときの処理を関数で渡す(Python 3.12で追加) |
onerror |
onexc の前身。3.12で非推奨になった |
onerror から onexc への変更は、受け取る引数が例外の情報のかたまりから例外そのものへ変わったものです。古い記事のコードをそのまま持ってくると警告が出るので、覚えておくと迷いません。
zipに固めるのは、make_archiveで1行¶
送付用にフォルダをzipにする作業も、shutilの担当です。make_archive を使うと1行で済みます。
import shutil
# report_2026-08.zip が作られる
path = shutil.make_archive("report_2026-08", "zip", root_dir="report")
print(path)
# 展開するとき
shutil.unpack_archive("report_2026-08.zip", "extracted")
第1引数には、拡張子を除いた名前を渡します。 .zip まで書くと report_2026-08.zip.zip という残念な名前になるので、ここだけ注意してください。
対応している形式は、手元で shutil.get_archive_formats() を実行すれば確認できます。標準では次の5つです。
| 形式の指定 | 中身 |
|---|---|
zip |
ZIPファイル |
tar |
圧縮なしのtar |
gztar |
gzipで圧縮したtar |
bztar |
bzip2で圧縮したtar |
xztar |
xzで圧縮したtar |
Windowsの相手に送るなら zip、Linuxのサーバーへ置くなら gztar が無難です。展開側の環境に合わせて選びましょう。
なお、外部の圧縮コマンドを呼び出す方法もあります。ただ、標準で足りるならshutilのほうが環境差に強いです。【関連記事】Pythonのsubprocessとは?外部コマンドをPythonから実行する基本を初心者向けに解説
知っていると助かる、小さな関数たち¶
shutilには、ファイル操作以外の便利な関数も同居しています。使用頻度の高い2つを紹介します。
1つは shutil.which() です。コマンドがその環境で使えるかどうかを調べて、見つかればその場所を、無ければ None を返します。
import shutil
if shutil.which("git") is None:
print("gitが入っていません")
外部コマンドに頼るスクリプトの冒頭に置いておくと、実行の途中で謎のエラーが出る事態を避けられます。
もう1つは shutil.disk_usage() です。指定した場所の容量を、合計・使用中・空きの3つで返してくれます。
大きなコピーを始める前に空き容量を確かめる、という使い方ができます。途中で容量が尽きて中途半端なファイルが残るのは、いちばん後始末が面倒な失敗です。
自動化したスクリプトを毎晩動かすなら、実行そのものも仕組みに任せてしまえます。【関連記事】Pythonのcronとは?定期実行の仕組みと設定方法を初心者向けに解説
今日から使える形にまとめておく¶
最後に、手を動かすときの判断だけに絞って整理します。
ファイル1つをコピーするなら copy2。フォルダごとなら copytree に dirs_exist_ok と ignore_patterns を添える。この2つで、コピーの用事はほぼ片づきます。
移動は move、圧縮は make_archive。どちらも引数は素直なので、覚えることは多くありません。
そして削除です。rmtree を書くときだけは、必ず消す対象を表示してから実行してください。 面倒に思えても、この習慣が最後に自分を助けます。
まずは、いらないファイルを1つ作って shutil.copy2() で複製するところから試してみましょう。ファイルの読み書きそのものに不安が残るなら、先に基本を押さえておくと理解が早くなります。【関連記事】Pythonのファイル読み書きとは?open()とwith文の基本を初心者向けに解説
ここまでお読みいただきありがとうございました。