Pythonでフォルダを作る、ファイルがあるか確かめる、いらないファイルを消す。どれもosとos.pathで書けますが、パスを文字列として扱う書き方では結合や分解のたびに関数を呼び分けることになります。標準ライブラリのpathlibなら、同じ処理をパスオブジェクトのメソッドとして書けます。この記事では作成・存在確認・結合・分解・削除・移動・検索という頻出処理を、掲載したコードをそのまま3.9から3.14の各系列で実行して確かめた結果とともに並べました。検索でよく見かけるpip install pathlibが不要な理由も、実際に入れると何が起きるかまで示します。
まとめ
pathlibはPython 3.4で標準ライブラリに入っているため、インストールは不要です。PyPIにあるpathlibパッケージは3.4より前の環境向けの移植版で、最後のバージョンは1.0.1(ソース配布は2014年9月)です。- フォルダ作成は
Path("a/b/c").mkdir(parents=True, exist_ok=True)の一行で足ります。parentsとexist_okを省くと、親が無いときと既にあるときにそれぞれ例外になります。 - パスの結合は
/演算子です。右側に絶対パスを置くと左側が捨てられるため、外部入力を結合する処理ではこの挙動が抜け道になります。 - 移動は
rename()ではなくreplace()を使うと、Windowsでも既存ファイルを上書きする挙動に揃います。削除はunlink(missing_ok=True)で存在確認を省けます。 - ディレクトリ探索は3.12で
Path.walk()、コピーと移動は3.14でPath.copy()とPath.move()が加わりました。それ以前の系列ではos.walk()とshutilが必要です。
ここからは作成、確認、分解、削除、検索の順に、実行して確かめたコードと引数の意味を並べます。
pip install pathlibが不要な理由とPyPI版を入れた場合の実害
「pathlib install」「python pathlib install」といった検索が一定数ありますが、結論としてインストール作業はありません。公式ドキュメントのpathlibのページには「Added in version 3.4」と記載があり、Python 3.4以降ならfrom pathlib import Pathだけで使えます。現行系列は3.14(3.14.7、2026年8月5日リリース)で、サポート期間が残っている3.10以降はすべて対象です。
PyPI版pathlibを入れたときに起きること
ややこしいのは、PyPIにpathlibという名前のパッケージが残っている点です。これはPython 3.4より前のバージョン向けに公開された移植版で、最後のバージョンは1.0.1、説明文の冒頭に「this backport module isn’t maintained anymore(このバックポートはもう保守されていない)」と書かれています。pip install pathlibは今でもエラーなく完了し、Python 3.13.14の仮想環境ではSuccessfully installed pathlib-1.0.1と表示されました。
ただし入った中身は動きません。1.0.1の10行目はfrom collections import Sequenceで、この書き方はPython 3.10で撤去されています。実際にこのファイルを読み込ませるとImportError: cannot import name 'Sequence' from 'collections'で止まります。3.9では通り、3.12・3.13・3.14では止まることを確認しました。
ここで誤解しやすいのは、入れた直後に何かが壊れるわけではない点です。通常のsys.pathの並びでは標準ライブラリのディレクトリがsite-packagesより先に来るため、import pathlibは標準ライブラリ側を読み込みます。壊れるのはsite-packagesを先に探索させたときで、PYTHONPATHにsite-packagesを指定した状態でimport pathlibすると前述のImportErrorでプログラムが起動しなくなります。動いているように見えて実行環境の作り方次第で落ちる依存が1つ増えるだけなので、入れる理由はありません。
入れてしまった環境の戻し方
pip uninstall pathlibで削除できます。requirements.txtやpyproject.tomlにpathlibが紛れ込んでいる場合はそちらも消してください。Python 2と3を両対応させる目的でpathlib2を使っている古いプロジェクトもありますが、こちらも最新は2.3.7.post1で2022年2月が最後の公開です。Python 3系だけを対象にするなら、どちらも標準ライブラリに置き換えられます。
フォルダ(ディレクトリ)作成の書き方
深い階層をまとめて作るmkdirの引数
Path.mkdir()にはparentsとexist_okという2つの引数があり、実務で使うのはほぼ両方をTrueにした形です。
from pathlib import Path
Path("data/2026/08").mkdir(parents=True, exist_ok=True)
print(Path("data/2026/08").is_dir())
実行するとTrueが出力され、data/2026/08が一度に作られます。parents=Trueが中間ディレクトリの作成、exist_ok=Trueが「既にあっても例外にしない」指定です。どちらの既定値もFalseなので、省略すると次のように失敗します。
from pathlib import Path
Path("data").mkdir(exist_ok=True)
try:
Path("data/x/y").mkdir()
except FileNotFoundError as e:
print("parents=False:", type(e).__name__, e)
try:
Path("data").mkdir()
except FileExistsError as e:
print("exist_ok=False:", type(e).__name__, e)
出力はparents=False: FileNotFoundError [Errno 2] No such file or directory: 'data/x/y'とexist_ok=False: FileExistsError [Errno 17] File exists: 'data'の2行です。バッチ処理で日付ごとの出力先を作るような場面なら、毎回parents=True, exist_ok=Trueを付けておくと分岐を書かずに済みます。
os.makedirsとの対応と使い分け
os.makedirs(path, exist_ok=True)はPath(path).mkdir(parents=True, exist_ok=True)と同じ結果になります。名前の対応がずれている点だけ注意してください。os.mkdir()は中間ディレクトリを作らない一段だけの作成で、Path.mkdir()の既定(parents=False)に相当します。つまりos側は関数名で作り分け、pathlib側は引数で作り分ける形です。新しく書くコードではpathlibに統一し、作成後のパスをそのまま/演算子で使い回すとよいでしょう。既存コードがosで統一されている場合は、無理に混在させずファイル単位で移行する方が差分を追いやすくなります。
ファイル作成と読み書きの書き方
空ファイル作成と一括書き込みの使い分け
中身のない目印ファイルを作るならtouch()、内容ごと書き出すならwrite_text()です。読み出しはread_text()で、open()とwithを書かずに1行で済みます。
from pathlib import Path
p = Path("data/2026/08/report.txt")
p.touch()
p.write_text("売上集計\n", encoding="utf-8")
print(p.read_text(encoding="utf-8"), end="")
出力は売上集計です。touch()は既存ファイルに対しては更新時刻だけを変え、内容は消しません。既にある場合にエラーにしたいときはtouch(exist_ok=False)とするとFileExistsErrorになります。逆にwrite_text()は既存の内容を上書きします。追記したい場合はこのメソッドでは書けないため、with p.open("a", encoding="utf-8") as f:のように追記モードで開いてください。画像やバイナリを扱うならread_bytes()とwrite_bytes()が対になります。ログのように書き続けるファイルであれば、サイズで自動的に切り替えるPythonのRotatingFileHandlerでログローテーション|maxBytes・backupCountの設定と複数プロセス対策の仕組みを使う方が確実です。
encodingの明示が必要な理由
write_text()とread_text()でencodingを省略すると、その環境のロケール設定が使われます。Linuxやmacでは実質UTF-8ですが、日本語版Windowsではcp932になり、同じコードが環境によって別のバイト列を書き出します。省略箇所はpython -X warn_default_encodingを付けて実行するとEncodingWarning: 'encoding' argument not specifiedとして検出できます。日本語を含むファイルを扱うならencoding="utf-8"を常に書いてください。なお、この前提はPython 3.15で変わります。UTF-8モードを既定にするPEP 686が採択済み(Status: Final、対象はPython 3.15)で、適用後はロケールに関わらずUTF-8が既定になります。それまでは明示が必要です。
存在確認とファイル種別の判定
exists・is_file・is_dirの使い分け
「あるかどうか」だけならexists()、「ファイルなのかフォルダなのか」まで区別するならis_file()とis_dir()です。
from pathlib import Path
p = Path("data/2026/08/report.txt")
print(p.exists(), p.is_file(), p.is_dir())
出力はTrue True Falseです。3つとも真偽値を返し、パスが存在しない場合は例外ではなくFalseになります。注意点はリンク切れのシンボリックリンクで、リンク自体は存在してもリンク先が無いとexists()はFalseを返します。リンクそのものの有無を見たい場合はis_symlink()を使ってください。
存在確認より例外処理が向く場面
存在確認をしてから開く書き方には、確認から実行までの間に別のプロセスがファイルを消す可能性が残ります。読み込みや削除のように「実行してみれば分かる」処理では、確認を挟まず例外で受ける方がこの隙間(TOCTOU)を踏みません。
from pathlib import Path
p = Path("data/2026/08/report.txt")
try:
text = p.read_text(encoding="utf-8")
except FileNotFoundError:
text = ""
print(len(text))
実行すると読み込んだ文字数が出力されます。ファイルが無ければFileNotFoundErrorが飛ぶので、空文字を既定値にして処理を続けています。存在確認が要るのは、無かったときに読み込み以外の処理(作成、別パスへの切り替え、利用者への通知)へ分岐したい場合です。
パス結合の書き方と絶対パスの落とし穴
スラッシュ演算子とjoinpath
パスの結合には/演算子を使います。区切り文字を自分で書かないため、WindowsとLinuxで書き分ける必要がありません。
from pathlib import Path
base = Path("/var/www/app")
print(base / "uploads" / "2026-08" / "report.csv")
print(base.joinpath("uploads", "2026-08"))
print(base / "/etc/passwd")
出力は/var/www/app/uploads/2026-08/report.csv、/var/www/app/uploads/2026-08、そして3行目が/etc/passwdです。要素を変数でまとめて渡したいときはjoinpath()を使うと同じ結果になります。文字列の+で連結すると区切り文字の重複や欠落を自分で管理することになるため、パス同士の連結には使わないでください。
絶対パスを結合したときの挙動
3行目だけ結果が直感に反します。Path("/var/www/app") / "/etc/passwd"は/var/www/app/etc/passwdにはならず、左側を捨てて/etc/passwdになります。これはos.path.join()と同じ仕様で、右側が絶対パスなら以降がルートとして扱われるためです。
アップロードされたファイル名やAPIのパラメータをそのまま結合している箇所では、この挙動によって想定した保存先の外に出られます。..を含むパスも同様で、結合しただけでは正規化されません。外部から受け取った値を結合する場合は、結合後にresolve()で正規化してから、基準ディレクトリ配下かどうかをis_relative_to()(Python 3.9で追加)で検査してください。基準からの相対パスを取り出すrelative_to()も同じ組み合わせで使います。攻撃の成立条件と検査の実装はディレクトリトラバーサルとは?攻撃の仕組みと正規化後検証による実装対策を解説で整理しています。
ファイル名・拡張子・親ディレクトリの取得
パスの分解は属性へのアクセスだけで済みます。os.path.splitext()とos.path.basename()を組み合わせる必要はありません。
from pathlib import Path
p = Path("/var/log/app/access.log.gz")
print(p.name, p.stem, p.suffix, p.suffixes)
print(p.parent, p.parts)
print(p.with_suffix(".bak"), p.with_stem("error"))
出力は次の3行です。1行目がaccess.log.gz access.log .gz ['.log', '.gz']、2行目が/var/log/app ('/', 'var', 'log', 'app', 'access.log.gz')、3行目が/var/log/app/access.log.bak /var/log/app/error.gzになります。
2行目のpartsはパスを区切り文字ごとに分けたタプルで、先頭要素にルートの/が入ります。階層の深さを数えたり、特定の階層名が含まれるかを調べたりする用途に向きます。stemは最後の拡張子だけを外すため、.log.gzのような二重拡張子ではaccess.logが残ります。すべての拡張子を取り除きたい場合はsuffixesの要素数だけwith_suffix("")を繰り返すか、name.split(".")[0]で先頭だけを取ってください。with_stem()はPython 3.9で追加された、拡張子を保ったままファイル名部分だけを差し替えるメソッドです。カレントディレクトリを返すPath.cwd()や、実行中スクリプトの位置から絶対パスを組み立てるPath(__file__).resolve().parentの使い方はPython pathlibの使い方|resolve・絶対パス取得・Path(__file__).resolve().parentとos.pathの違いにまとめています。
ファイルの削除・移動・コピー
削除はunlink・rmdir・rmtreeの3種類
ファイルの削除はunlink()、空のディレクトリはrmdir()、中身ごと消すディレクトリはshutil.rmtree()と対象で分かれます。
from pathlib import Path
import shutil
Path("data/2026/08/report.txt").unlink(missing_ok=True)
Path("data/2026/08").rmdir()
shutil.rmtree("data/2026", ignore_errors=True)
print(Path("data/2026").exists())
最後にFalseが出力され、ディレクトリごと消えたことが分かります。unlink()は対象が無いとFileNotFoundErrorになりますが、Python 3.8で追加されたmissing_ok=Trueを付けると無視されます。rmdir()は空でないディレクトリに対してOSError(ENOTEMPTY。errnoはmacOSやBSDが66、Linuxは39)を返すため、中身ごと消す用途には使えません。pathlibには再帰削除のメソッドが無いので、そこだけはshutilを併用します。
renameとreplaceの上書き挙動の違い
移動と改名はどちらも同じ操作ですが、移動先に既存ファイルがあるときの挙動が分かれます。
from pathlib import Path
Path("data/a.txt").write_text("A", encoding="utf-8")
Path("data/b.txt").write_text("B", encoding="utf-8")
Path("data/a.txt").replace("data/b.txt")
print(Path("data/b.txt").read_text(encoding="utf-8"))
出力はAで、b.txtの中身がa.txtの内容に置き換わりました。公式ドキュメントによるとrename()はUnixでは既存ファイルを黙って置き換える一方、WindowsではFileExistsErrorになります。replace()はどちらのプラットフォームでも上書きします。移植性を考えるなら、上書きしてよい場面ではreplace()、上書きを避けたい場面では事前にexists()で分岐する形に統一してください。
コピーはshutil、3.14からはPath.copy
コピーは長らくpathlibの守備範囲外で、shutil.copy2()を使うのが定番でした。Python 3.14でPath.copy()とPath.move()、および対象を既存ディレクトリの中へ入れるcopy_into()とmove_into()が追加されています。
from pathlib import Path
Path("data/src.txt").write_text("copy", encoding="utf-8")
Path("data/src.txt").copy(Path("data/dst.txt"))
Path("data/dst.txt").move(Path("data/moved.txt"))
print(Path("data/moved.txt").read_text(encoding="utf-8"))
3.14.6ではcopyと出力されますが、3.13.14で同じコードを実行するとAttributeError: 'PosixPath' object has no attribute 'copy'になります。書く前に対象環境のバージョンを確認してください。もう1点、Path.copy()の引数はcopy(target, *, follow_symlinks=True, preserve_metadata=False)で、既定では更新時刻やパーミッションを引き継ぎません。shutil.copy2()から移行する場合はpreserve_metadata=Trueの明示が必要です。3.14で入ったその他の変更点はPython 3.14の新機能まとめ|JITコンパイラ・フリースレッド版(GIL撤廃)・t-stringを最新版で解説で扱っています。
ファイル検索とディレクトリ探索
globとrglobの書き分け
特定の条件に合うファイルを集めるにはglob()を使います。直下だけを見るか、階層をたどるかでメソッドが変わります。
from pathlib import Path
root = Path("src")
print([str(p) for p in sorted(root.glob("*.py"))])
print([str(p) for p in sorted(root.rglob("*.py"))])
出力は['src/main.py']と['src/main.py', 'src/pkg/util.py']です。rglob("*.py")はglob("**/*.py")と同じ結果になります。どちらもジェネレータを返すので、件数を数えたりソートしたりする場合はlist()やsorted()で確定させてください。Python 3.13からはglob()にrecurse_symlinks引数が加わり、シンボリックリンクをたどるかどうかを指定できます。
Python 3.12で追加されたPath.walk
ディレクトリ単位で処理したい場合はos.walk()と同じ形のPath.walk()が使えます。
from pathlib import Path
for dirpath, dirnames, filenames in Path("src").walk():
print(dirpath, sorted(filenames))
出力はsrc ['main.py']とsrc/pkg ['notes.md', 'util.py']の2行です。追加は3.12で、3.11ではAttributeError: 'PosixPath' object has no attribute 'walk'になります。3.11以前を保守している環境ではos.walk()のままにしてください。
大量ファイルの列挙でpathlibを避ける場面
pathlibに統一すべきでない処理もあります。Path.iterdir()で得た各パスにis_file()を呼ぶと、パスごとにシステムコールが走ります。os.scandir()はディレクトリを読んだ時点の情報を各エントリが保持するため、同じ判定を追加のシステムコールなしで返します。次のコードは同じ判定を7回ずつ実行し、所要時間の中央値をミリ秒で表示します。
import os
import statistics
import time
from pathlib import Path
d = Path("many") # 5,000個のファイルを置いたディレクトリ
def by_pathlib():
return sum(1 for p in d.iterdir() if p.is_file())
def by_scandir():
with os.scandir(d) as it:
return sum(1 for e in it if e.is_file())
for name, func in [("iterdir+is_file", by_pathlib), ("os.scandir", by_scandir)]:
times = []
for _ in range(7):
started = time.perf_counter()
count = func()
times.append(time.perf_counter() - started)
print(name, count, round(statistics.median(times) * 1000, 1), "ms")
5,000個のファイルを置いたディレクトリを対象に、macOS(APFS)のPython 3.13.14で実行した出力はiterdir+is_file 5000 30.7 msとos.scandir 5000 5.4 msでした。同じ環境でも実行ごとに1〜2ミリ秒は振れますが、6倍前後の差は繰り返しても変わりません。数十から数百件の処理では体感差になりませんが、数万件を毎回走査するバッチではos.scandir()を選ぶ理由があります。可読性を優先してpathlibに寄せ、走査が処理時間の大半を占めることが計測で分かった箇所だけos.scandir()へ落とす、という順番が扱いやすいでしょう。
os・os.pathからの移行早見表
既存コードを読み替えるときの対応です。左を右に置き換えれば、印を付けた2行を除いてそのまま動きます。
| 目的 | os ・ os.path | pathlib |
|---|---|---|
| ディレクトリ作成 | os.makedirs(p, exist_ok=True) |
Path(p).mkdir(parents=True, exist_ok=True) |
| パス結合 | os.path.join(a, b) |
Path(a) / b |
| 存在確認 | os.path.exists(p) |
Path(p).exists() |
| ファイル判定 | os.path.isfile(p) |
Path(p).is_file() |
| ディレクトリ判定 | os.path.isdir(p) |
Path(p).is_dir() |
| ファイル名 | os.path.basename(p) |
Path(p).name |
| 拡張子なしの名前 | os.path.splitext(os.path.basename(p))[0] |
Path(p).stem |
| 拡張子 | os.path.splitext(p)[1] |
Path(p).suffix |
| 親ディレクトリ | os.path.dirname(p) |
Path(p).parent |
| 絶対パス化 ※挙動差あり | os.path.abspath(p) |
Path(p).resolve() |
| ファイル削除 | os.remove(p) |
Path(p).unlink() |
| 直下の一覧 | os.listdir(d) |
Path(d).iterdir() |
| 再帰探索 ※3.12以降 | os.walk(d) |
Path(d).walk() |
| ファイル読み込み | open(p).read() |
Path(p).read_text(encoding="utf-8") |
印を付けた「絶対パス化」は、os.path.abspath()が文字列として正規化するだけなのに対し、Path.resolve()はシンボリックリンクを解決した実体のパスを返します。リンクを張った運用をしている環境では結果が変わるため、そのまま置き換えないでください。この差の詳細はPython pathlibの使い方|resolve・絶対パス取得・Path(__file__).resolve().parentとos.pathの違いで扱っています。表に無いのはコピーと再帰削除で、3.13以前ではshutil.copy2()とshutil.rmtree()の併用が必要です。プロジェクト全体で書き方をそろえるなら、PEP8とは?Pythonコーディング規約の基本ルールとチェックツール(Ruff対応)で触れているRuffのようなツールで一度に点検すると差分が読みやすくなります。
よくある質問
pathlibはpip installが必要ですか?
不要です。Python 3.4以降の標準ライブラリなので、from pathlib import Pathだけで使えます。PyPIにあるpathlibパッケージは3.4より前の環境向けの移植版で、インストール自体は成功しますが、通常は標準ライブラリ側が優先されるため効果はありません。PYTHONPATHなどでsite-packagesを先に探索させた場合だけ、from collections import Sequenceを使っている中身がPython 3.10以降でImportErrorになります。誤って入れた場合はpip uninstall pathlibで削除してください。
フォルダを作るときos.makedirsとPath.mkdirのどちらを使うべきですか?
新規に書くコードではPath(p).mkdir(parents=True, exist_ok=True)を推奨します。作成後のパスをそのまま/演算子で使い回せるためです。既存コードがosで統一されている場合は、無理に混在させず、ファイル単位で移行する方が差分を追いやすくなります。
ファイルが存在するか確認するにはどう書きますか?
Path("path/to/file").exists()で真偽値が返ります。ファイルかディレクトリかを区別するならis_file()とis_dir()を使ってください。確認直後に読み込む処理であれば、確認を挟まずFileNotFoundErrorをtryで受ける方が、確認と実行の間の状態変化に強くなります。
拡張子を除いたファイル名だけを取り出すには?
Path("/var/log/access.log").stemでaccessが得られます。拡張子はsuffix、ファイル名全体はnameです。ただしaccess.log.gzのような二重拡張子ではstemはaccess.logを返します。すべて外したい場合はsuffixesで拡張子の一覧を取得し、その数だけwith_suffix("")を適用してください。
ファイル削除でFileNotFoundErrorを避けるにはどうしますか?
Path(p).unlink(missing_ok=True)とすると、対象が無い場合でも例外になりません。この引数はPython 3.8で追加されました。3.7以前ではtryとexcept FileNotFoundErrorで囲む必要があります。ディレクトリに対してはunlink()ではなくrmdir()、中身がある場合はshutil.rmtree()を使ってください。