---
title: "Pythonでフォルダ作成・ファイル作成をする方法｜存在確認・削除・移動をpathlibで書く"
url: "https://www.issoh.co.jp/tech/details/3292/"
published: 2024-08-06
updated: 2026-08-11
categories: ["Python"]
publisher: "株式会社一創"
---

# Pythonでフォルダ作成・ファイル作成をする方法｜存在確認・削除・移動をpathlibで書く

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の設定と複数プロセス対策](/tech/details/3696/)の仕組みを使う方が確実です。

### 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()`も同じ組み合わせで使います。攻撃の成立条件と検査の実装は[ディレクトリトラバーサルとは？攻撃の仕組みと正規化後検証による実装対策を解説](/tech/details/16368/)で整理しています。

## ファイル名・拡張子・親ディレクトリの取得

パスの分解は属性へのアクセスだけで済みます。`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の違い](/tech/details/5986/)にまとめています。

## ファイルの削除・移動・コピー

### 削除は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を最新版で解説](/tech/details/9409/)で扱っています。

## ファイル検索とディレクトリ探索

### 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の違い](/tech/details/5986/)で扱っています。表に無いのはコピーと再帰削除で、3.13以前では`shutil.copy2()`と`shutil.rmtree()`の併用が必要です。プロジェクト全体で書き方をそろえるなら、[PEP8とは？Pythonコーディング規約の基本ルールとチェックツール（Ruff対応）](/tech/details/3032/)で触れている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()`を使ってください。

## 関連記事

- [Python pathlibの使い方｜resolve・絶対パス取得・Path(\_\_file\_\_).resolve().parentとos.pathの違い](/tech/details/5986/)
- [ディレクトリトラバーサルとは？攻撃の仕組みと正規化後検証による実装対策を解説](/tech/details/16368/)
- [Python 3.14の新機能まとめ｜JITコンパイラ・フリースレッド版(GIL撤廃)・t-stringを最新版で解説](/tech/details/9409/)
- [PythonのRotatingFileHandlerでログローテーション｜maxBytes・backupCountの設定と複数プロセス対策](/tech/details/3696/)
- [PEP8とは？Pythonコーディング規約の基本ルールとチェックツール（Ruff対応）](/tech/details/3032/)

---

出典: [Pythonでフォルダ作成・ファイル作成をする方法｜存在確認・削除・移動をpathlibで書く](<https://www.issoh.co.jp/tech/details/3292/>)（株式会社一創）
