23 人が閲覧(直近 30 日) Python

Python pathlibの使い方|Path(__file__).resolve().parentの意味とresolve()・absolute()の違い

Python pathlibの使い方|Path(__file__).resolve().parentの意味とresolve()・absolute()の違い

Path(__file__).resolve().parent は「いま実行中のファイルが実際に置かれているディレクトリ」を絶対パスで返す書き方です。カレントディレクトリがどこでも同じフォルダを指すので、設定ファイルやデータを読み込むスクリプトの定番になっています。

ただし、Python 3.9以降は __file__ 自体が絶対パスなので、resolve() が効くのはシンボリックリンク経由の起動と、パスに .. が混ざるときに限られます。この記事では、その差をPython 3.14.6で実行した出力で示し、absolute()・os.path.abspath() との違い、stem・suffix、バージョンで挙動が変わった点を整理します。

まとめ

  • Path(__file__).resolve().parent は実行中ファイルの実体があるディレクトリ。2階層上は .parents[1](.parent.parent と同じ)
  • resolve() は絶対パス化に加えて .. の除去とシンボリックリンクの解決を行う。absolute() はカレントディレクトリを前に付けるだけ
  • os.path.abspath() は .. を文字列として消すので、リンクが絡むと resolve() と結果が食い違う
  • REPLとJupyterには __file__ が無い。PyInstallerでは、実行ファイルの隣の設定は sys.executable、バンドル内のデータは __file__ を基準にする
  • 3.13で「**」で終わるglobとリンクのループ時の例外、3.14で suffix の扱いが変わった。古い記事のコードは版を確認してから使う

フォルダ作成・ファイル作成・削除の手順はPythonでフォルダ作成・ファイル作成をする方法|存在確認・削除・移動をpathlibで書くにまとめています。この記事はパスの取り出し方と解決の仕組みを扱います。

Path(__file__).resolve().parentが返すディレクトリ

チェーンを左から読むと、Path(__file__) でファイルのパスをPathオブジェクトにし、resolve() で実体の絶対パスに直し、parent で1つ上=ファイルを含むディレクトリを取り出しています。典型的な使い方は、スクリプトと同じ場所にある設定ファイルの読み込みです。

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent      # このファイルがあるディレクトリ
ROOT_DIR = Path(__file__).resolve().parents[1]  # 1つ上(parent.parent と同じ)
# このスクリプトと同じディレクトリに、UTF-8の config.toml を用意しておく
config = BASE_DIR / "config.toml"
print(config.read_text(encoding="utf-8"))

open("config.toml") のように相対パスで書くと、Pythonを起動したカレントディレクトリを基準に探します。cronやIDEの実行設定で起動場所が変わると FileNotFoundError になるため、ファイル位置を基準にするのが安全です。

resolve()を省いて結果が変わるケース

Python 3.9で、コマンドラインで指定したスクリプトの __file__ が絶対パスになりました(What’s New in Python 3.9)。インポートされたモジュールの __file__ は3.4から絶対パスです。そのため普通に python proj/app/main.py と起動する限り、Path(__file__).parent だけでも絶対パスが返ります。

結果が変わるのは、シンボリックリンク経由で起動したときです。bin/run を proj/app/main.py へのリンクにして、同じスクリプトを2通りに起動しました。

# proj/app/main.py
from pathlib import Path

print(Path(__file__))
print(Path(__file__).parent)
print(Path(__file__).resolve().parent)
$ python proj/app/main.py
/private/tmp/demo/proj/app/main.py
/private/tmp/demo/proj/app
/private/tmp/demo/proj/app
$ python bin/run
/private/tmp/demo/bin/run
/private/tmp/demo/bin
/private/tmp/demo/proj/app

リンク経由では parent がリンクの置き場所(bin)を指し、隣にあるはずの設定ファイルを見失います。resolve() を挟んだ行だけが実体の proj/app を返しました。スクリプトを /usr/local/bin などにリンクして呼び出す運用では省略しないでください。

parents[1]とparent.parentによる2階層上の取得

parents は祖先ディレクトリの並びで、parents[0] が parent、parents[1] が parent.parent に当たります。3.10からスライスと負のインデックスが使えます。

from pathlib import Path, PurePosixPath

p = PurePosixPath("/home/taro/project/src/app/main.py")
print(p.parent)                 # /home/taro/project/src/app
print(p.parent.parent)          # /home/taro/project/src
print(p.parents[1])             # parent.parent と同じ
print(p.parents[-1])            # 3.10以降:負のインデックスで最上位(/)
print(list(p.parents[:3]))      # 3.10以降:スライス
print(PurePosixPath("foo/..").parent)   # 字句的な処理なので foo になる
print(Path("main.py").parent)   # 相対パスの親は "."

最後の2行が落とし穴です。parent は文字列を区切りで切るだけなので、foo/.. の親は foo になり、相対パス main.py の親は . のままです。公式ドキュメントも、ファイルシステム上で上へたどるなら先に resolve() を呼ぶよう勧めています。

__file__が使えない環境の代替

対話モード(REPL)、python -c、Jupyter Notebookでは __file__ が定義されず NameError になります。PyInstallerで固めた実行ファイルでは、sys.executable が実行ファイル自身、sys._MEIPASS がバンドルの展開先(one-file形式では一時フォルダ)を指します(PyInstaller 6.22.3のRun-time Information)。実行ファイルの隣に置いた設定を読むなら sys.executable 側を基準にします。

import sys
from pathlib import Path

def app_dir() -> Path:
    if getattr(sys, "frozen", False):           # PyInstaller などで固めた実行ファイル
        return Path(sys.executable).resolve().parent
    try:
        return Path(__file__).resolve().parent
    except NameError:                           # ファイル位置ではなく現在の作業ディレクトリを使う
        return Path.cwd()

print(app_dir())

ファイルとして実行しても、python -c に同じコードを渡しても、どちらもディレクトリを返すことを3.14.6で確認しました。

resolve()・absolute()・os.path.abspath()の違い

3つとも「絶対パスを得る」手段ですが、.. とシンボリックリンクの扱いが違います。存在しないパスを渡した結果が次のとおりです(macOSで実行)。表示例のカレントディレクトリは /private/tmp/demo で、dataとconfig.tomlが存在しない状態を前提にしています。

import os
from pathlib import Path

p = Path("data/../config.toml")        # data と config.toml はまだ無くてもよい
print(p.absolute())                     # .. を残す
print(os.path.abspath(p))               # .. を文字列として消す
print(p.resolve())                      # .. を消し、シンボリックリンクも解決
print(Path("/tmp").resolve())           # macOS では /tmp が /private/tmp へのリンク
try:
    p.resolve(strict=True)
except FileNotFoundError as e:
    print("strict=True:", type(e).__name__)
/private/tmp/demo/data/../config.toml
/private/tmp/demo/config.toml
/private/tmp/demo/config.toml
/private/tmp
strict=True: FileNotFoundError
方法 ..の除去 リンク解決 ファイルに触る
Path.absolute() しない しない 触らない
os.path.abspath() 文字列で消す しない 触らない
Path.resolve() する する 触る
os.path.realpath() する する 触る

os.path.abspath() の除去は危険側に倒れます。data が別の場所へのリンクだった場合、data/.. の実体はリンク先の親ですが、abspath() は文字列上で打ち消してしまいます。公式ドキュメントの対応表も、abspath() はリンクを解決せずに .. を消すため意味が変わりうると注記しています。pathlibで .. とシンボリックリンクを解決するなら resolve() を使います。

逆に resolve() はファイルシステムに問い合わせるので、ネットワークドライブや大量のパスではコストがかかります。表示用に絶対パスへ直すだけなら absolute() で足ります。

存在しないパスとstrict=True

resolve() の既定は strict=False で、存在しない部分はそのまま後ろにつなげて返します。これから作るファイルのパスを組み立てるときはこれで困りません。存在を前提に処理するなら strict=True にすると、見つからない時点で FileNotFoundError(OSError の一種)が出ます。

シンボリックリンクがループしている場合の扱いは3.13で変わりました。3.12以前は strict の値に関係なく RuntimeError、3.13以降は非strictなら例外なしでパスを返し、strictなら OSError です。3.10〜3.15.0b4で実行して、この境目を確認しました。except RuntimeError でループを捕まえていたコードは、3.13以降では素通りします。

ファイル名・拡張子の取り出し(name・stem・suffix)

「拡張子なしのファイル名」は stem、拡張子は suffix です。多重拡張子とドットファイルで結果が直感とずれます。

from pathlib import PurePosixPath as P

for s in ["report.csv", "backup.tar.gz", ".bashrc", "draft."]:
    p = P("/srv/files") / s
    print(f"{s:14} name={p.name!r:16} stem={p.stem!r:14} suffix={p.suffix!r:6} suffixes={p.suffixes}")
report.csv     name='report.csv'     stem='report'       suffix='.csv' suffixes=['.csv']
backup.tar.gz  name='backup.tar.gz'  stem='backup.tar'   suffix='.gz'  suffixes=['.tar', '.gz']
.bashrc        name='.bashrc'        stem='.bashrc'      suffix=''     suffixes=[]
draft.         name='draft.'         stem='draft'        suffix='.'    suffixes=['.']

上は3.14.6の出力です。backup.tar.gz の stem は backup.tar なので、backup だけが欲しいなら、外す拡張子が決まっている前提で name.removesuffix(".tar.gz")(3.9以降)を使います。最初のドットで切る方法は .bashrc を空文字にしてしまいます。.bashrc はドット始まりでも拡張子扱いされません。末尾がドットだけの draft. は、3.13.14では stem='draft.'・suffix='' でした。3.14から単独の「.」も有効な拡張子として扱われるようになったためで、拡張子の有無で振り分ける処理は版によって結果が変わります。

拡張子の付け替えは with_suffix(".json")、名前部分だけの変更は with_stem("new")(3.9以降)です。

パスの結合と相対パスへの変換

結合は / 演算子か joinpath() です。右側に絶対パスを渡すと左側が捨てられる点に注意してください。ユーザー入力をそのまま結合すると、意図しない場所を指せてしまいます。

from pathlib import PurePosixPath as P

base = P("/srv/app")
print(base / "logs" / "app.log")        # /srv/app/logs/app.log
print(base.joinpath("logs", "app.log"))  # 同上
print(base / "/etc/hosts")               # 右側が絶対パスだと左側は捨てられる
print(P("/srv/app/logs/app.log").relative_to(base))            # logs/app.log
print(P("/srv/data/x.csv").relative_to(base, walk_up=True))    # 3.12以降:../data/x.csv
try:
    P("/srv/data/x.csv").relative_to(base)
except ValueError as e:
    print("ValueError")

relative_to() は既定では基準の配下にないと ValueError です。3.12で加わった walk_up=True を付けると .. を使った相対パスを返します。これも文字列上の計算なので、リンクを含む場合は先に両方を resolve() しておきます。なお3.12以前に許されていた relative_to("a", "b") のような複数引数は、3.14で削除されました。

Windowsのパスは PureWindowsPath で、macOSやLinux上でも挙動を確かめられます。

from pathlib import PureWindowsPath

p = PureWindowsPath(r"C:\Users\taro\data\sales.csv")
print(p.parent)        # C:\Users\taro\data
print(p.drive, p.stem) # C: sales
print(p.as_posix())    # C:/Users/taro/data/sales.csv

存在確認・読み書き・検索のメソッド早見表

よく使うメソッドと、引数が増えた版をまとめます。フォルダ作成や削除の詳しい手順は、前掲の「Pythonでフォルダ作成・ファイル作成をする方法」で扱っています。

用途 メソッド 版の注意
存在確認 exists() 3.12で follow_symlinks
種別判定 is_file() is_dir() 3.13で follow_symlinks
テキスト読込 read_text() 3.13で newline
テキスト書込 write_text() 上書きのみ・3.10で newline
バイナリ read_bytes() write_bytes() 3.5から
空ファイル作成 touch() 既定では内容を保ち時刻を更新
削除 unlink() 3.8で missing_ok
再帰検索 rglob() 3.12で case_sensitive
ツリー走査 walk() 3.12から
コピー・移動 copy() move() 3.14から

write_text() には追記モードがありません。追記は open("a") を使います。

from pathlib import Path

log = Path("app.log")
log.write_text("1行目\n", encoding="utf-8")        # 上書き(ファイルが無ければ作る)
with log.open("a", encoding="utf-8") as f:           # 追記は open("a") を使う
    f.write("2行目\n")
print(log.read_text(encoding="utf-8"), end="")
print(sorted(p.name for p in Path(".").rglob("*.log")))  # サブフォルダまで再帰検索
log.unlink(missing_ok=True)                           # 3.8以降:無くても例外にしない

encoding を省くとOSのロケール依存の文字コードで読み書きされ、Windowsで日本語が化ける原因になります。明示しておけば環境差が出ません。Python 3.15ではUTF-8モードが既定になるPEP 686が入る予定で、省略時の挙動そのものが版で変わります。ログの世代管理まで必要ならPythonのRotatingFileHandlerでログローテーション|maxBytes・backupCountの設定と複数プロセス対策も参照してください。

os.pathとの対応と使い分け

既存コードを読み替えるときの対応です。公式ドキュメントの対応表から、この記事で扱った関数を抜き出しました。

os / os.path pathlib
os.path.dirname(__file__) Path(__file__).parent
os.path.basename() .name
os.path.splitext() .stem .suffix
os.path.join() / joinpath()
os.path.abspath() absolute()(..は残る)
os.path.realpath() resolve()
os.path.relpath() relative_to()
os.getcwd() Path.cwd()
os.path.exists() exists()

この対応表は完全な互換表ではありません。たとえば os.path.relpath() は基準の配下でなくても相対パスを返しますが、既定の relative_to() は ValueError になります。末尾に区切りがあるパスの basename() と name なども結果が異なります。os.path.abspath(os.path.dirname(__file__)) を機械的に Path(__file__).parent.absolute() に直すと、.. が残ります。リンクと .. を畳みたい元の意図なら resolve() に寄せてください。

新規コードはpathlibで書き、open()・shutil・subprocess などにはPathをそのまま渡せます。ただし対応開始版は関数・引数・OSによって異なり、subprocess の args でPathを使えるのは、shell=False の場合、POSIXでは3.6以降、Windowsでは3.8以降です。文字列しか受け付けない古いライブラリに渡すときだけ str(path) で変換します。スタイル面の規約はPEP8とは?Pythonコーディング規約の基本ルールとチェックツール(Ruff対応)にまとめています。

Python 3.10〜3.15で結果が変わるpathlibの挙動

pathlibは毎年のように引数や戻り値が変わっており、本記事で扱うメソッドにも、Pythonのバージョンによって結果が変わるものがあります。3.10.20・3.11.15・3.12.13・3.13.14・3.14.6・3.15.0b4(macOS)で同じコードを実行して確かめた差分です。

変更点 変わった版 以前の挙動
parents のスライス・負インデックス 3.10 IndexError・TypeError
relative_to(walk_up=True) 3.12 引数なし
walk()・is_junction() 3.12 メソッドなし
glob(“**”) がファイルも返す 3.13 ディレクトリだけ
リンクのループで resolve() 3.13 常に RuntimeError
full_match()・from_uri() 3.13 メソッドなし
suffix が単独の「.」を認識 3.14 空文字
copy()・move()・info 3.14 shutil を使う
relative_to の複数引数 3.14で削除 結合して解釈
is_reserved() 3.15で削除 3.13から非推奨

影響が大きいのは glob("**") です。同じ g フォルダ(g/sub/f.txt と g/top.py を配置)に対して、3.12.13は ['g', 'g/sub']、3.13.14は ['g', 'g/sub', 'g/sub/f.txt', 'g/top.py'] を返しました。ディレクトリだけを列挙するつもりで「**」を使っていたコードは、3.13に上げるとファイルまで処理対象になります。ディレクトリに限るなら公式どおり末尾に区切りを付けて glob("**/") と書きます。

サポート期間の面では、3.9は2025年10月に終了し、3.10も2026年10月ごろにサポート終了予定です(PEP 619)。3.15.0の正式版は2026年10月1日の予定です(PEP 790)。この表の3.12以降の機能を使うなら、実行環境の版を先に揃えてください。各版の変更全体はPython 3.13の新機能まとめ|JIT・フリースレッド・対話型シェルの要点と3.14との違い【2026年版】とPython 3.14の新機能まとめ|JITコンパイラ・フリースレッド版(GIL撤廃)・t-stringを最新版で解説で解説しています。

よくある質問

Path(__file__).resolve().parentとPath(__file__).parentはどちらを使うべきですか?

resolve()付きを使ってください。Python 3.9以降はどちらも絶対パスを返しますが、シンボリックリンク経由で起動されるとparentだけではリンクの置き場所を指し、実体のディレクトリを取れません。

pathlibで2つ上のディレクトリを取るには?

Path(__file__).resolve().parents[1]、またはparent.parentです。どちらも同じパスを返します。parentsは3.10以降ならparents[-1]で最上位も取れます。

resolve()とabsolute()の違いは何ですか?

absolute()はカレントディレクトリを前に付けるだけで、..もシンボリックリンクもそのまま残します。resolve()はファイルシステムに問い合わせて..を除去し、リンクを実体に置き換えます。

拡張子なしのファイル名を取るには?

stem属性です。ただしbackup.tar.gzのstemはbackup.tarです。既知の多重拡張子.tar.gzを外すならname.removesuffix(“.tar.gz”)を使います。最初のドットで区切る処理は、.bashrcを空文字にし、名前に含まれるドット以降も削除するため、汎用的な拡張子除去には使えません。

pathlibはpip installが必要ですか?

不要です。Python 3.4から標準ライブラリに含まれています。PyPIにある同名パッケージは旧版のバックポートで、入れる必要はありません。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.07 テックブログ ファインズの不正アクセスと153万件の予約者情報|予約システムを使う店舗の報告と通知の手順
  2. 2026.10.19 テックブログ コミックシーモア不正アクセスで会員情報流出:再開時期・パスワード再設定と運営側のログ点検
  3. 2026.10.16 テックブログ バーガーキング情報漏洩どうしたらいい?約1,118万件の対象確認・補償・退会とメールの真偽
  4. 2026.10.15 テックブログ 銀のさら不正アクセス:最大650万件の対象確認と窓口、パスワード変更・退会の手順
  5. 2026.10.14 テックブログ ブックオフ不正アクセス:約638万人の対象確認と本物メールの見分け方

RELATED POSTS 関連記事

目次