pathlib statt os.path: moderne Dateipfad-Verarbeitung
1. Übersicht
Viel älterer Python-Code behandelt Dateipfade mit os.path.join, os.path.splitext, os.listdir… Diese Funktionen akzeptieren und geben Strings zurück, weshalb der Code lang, schwer lesbar und fehleranfällig wird. Das pathlib-Modul (seit Python 3.4 verfügbar) repräsentiert Dateipfade mit einem Path-Objekt, das ausreichend Methoden zum Verbinden, Teilen, Lesen, Schreiben und Durchsuchen von Dateien bietet.
Nach diesem Artikel kannst du den größten Teil deines os.path-Codes zu pathlib konvertieren und die häufigen Stolpersteine kennen. Die Beispiele in diesem Artikel verwenden Python 3.12.
2. Path-Objekte erstellen
Path ist der Startpunkt für jede Operation:
from pathlib import Path
reports = Path("data/reports")
home = Path.home()
cwd = Path.cwd()
print(reports)
print(type(reports))
print(home)
data/reports
<class 'pathlib.PosixPath'>
/home/minh
Erklärung:
Path("data/reports")erstellt ein Path-Objekt. Dabei finden noch keine Operationen auf der Festplatte statt. Das Verzeichnis muss nicht existieren.- Auf Linux/macOS gibt
PatheinPosixPath-Objekt zurück. Auf Windows gibt es einWindowsPath-Objekt zurück. Du brauchst dich nicht um die konkrete Klasse zu kümmern. Path.home()undPath.cwd()ersetzenos.path.expanduser("~")undos.getcwd().
Wenn du nur mit Pfad-Strings arbeiten musst und nicht auf echte Dateien zugreifst, verwendest du PurePath, PurePosixPath oder PureWindowsPath. Das ist praktisch, um Windows-Pfade zu verarbeiten, wenn du unter Linux läufst.
3. Pfade mit dem Operator / verbinden
Das ist die offensichtlichste Änderung. Statt verschachtelte os.path.join-Aufrufe verwendest du den Operator /:
import os
from pathlib import Path
# Alte Art
old = os.path.join("data", "orders", "2026", "order_1001.json")
# Neue Art
base = Path("data")
new = base / "orders" / "2026" / "order_1001.json"
print(old)
print(new)
print(new == Path(old))
data/orders/2026/order_1001.json
data/orders/2026/order_1001.json
True
Der Operator / akzeptiert sowohl Path als auch str auf der rechten Seite. Das Ergebnis ist immer ein Path, sodass du weiterverketten kannst. Das Trennzeichen wechselt automatisch je nach Betriebssystem: / unter Linux, \ unter Windows.
Wenn die Anzahl der Komponenten zur Laufzeit variabel ist, verwendest du joinpath:
parts = ["orders", "2026", "order_1001.json"]
path = Path("data").joinpath(*parts)
print(path) # data/orders/2026/order_1001.json
4. Pfadkomponenten extrahieren
os.path benötigt dafür mehrere Funktionen: basename, dirname, splitext. Mit pathlib sind es nur Attribute:
from pathlib import Path
invoice = Path("/srv/shop/invoices/2026/invoice_042.final.pdf")
print(invoice.name) # vollständiger Dateiname
print(invoice.stem) # Name ohne letzte Erweiterung
print(invoice.suffix) # letzte Erweiterung
print(invoice.suffixes) # alle Erweiterungen
print(invoice.parent) # übergeordnetes Verzeichnis
print(invoice.parents[1])
print(invoice.parts)
invoice_042.final.pdf
invoice_042.final
.pdf
['.final', '.pdf']
/srv/shop/invoices/2026
/srv/shop/invoices
('/', 'srv', 'shop', 'invoices', '2026', 'invoice_042.final.pdf')
Erklärung:
stemundsuffixteilen nur am letzten Punkt auf. Bei.tar.gz-Dateien musst dusuffixesverwenden.parententsprichtos.path.dirname.parentsist eine Sequenz von übergeordneten Verzeichnissen, die bis zur Wurzel aufsteigt.partsteilt den Pfad in ein Tuple auf, nützlich zum Vergleichen einzelner Ebenen.
Um einen neuen Pfad mit anderem Namen oder anderer Erweiterung zu erstellen, verwendest du die with_*-Methoden:
invoice = Path("invoices/invoice_042.pdf")
print(invoice.with_suffix(".xml")) # invoices/invoice_042.xml
print(invoice.with_stem("invoice_043")) # invoices/invoice_043.pdf
print(invoice.with_name("summary.csv")) # invoices/summary.csv
Diese Methoden ändern den Dateinamen nicht auf der Festplatte. Sie geben nur einen neuen Path zurück, da Path unveränderbar ist.
5. Dateien lesen und schreiben
Bei kleinen Dateien brauchst du nicht open() zu verwenden:
from pathlib import Path
config = Path("app_config.txt")
config.write_text("currency=VND\ntimeout=30\n", encoding="utf-8")
content = config.read_text(encoding="utf-8")
print(content.splitlines())
logo = Path("logo.bin")
logo.write_bytes(b"\x89PNG")
print(logo.read_bytes())
['currency=VND', 'timeout=30']
b'\x89PNG'
write_textöffnet die Datei, schreibt und schließt sie. Alte Dateien werden überschrieben.read_textliest den gesamten Inhalt als String.write_bytesundread_bytesarbeiten ähnlich mit Binärdaten.
Bei großen Dateien oder wenn du zeilenweise lesen möchtest, verwendest du immer noch einen Context Manager. Path hat eine open-Methode:
with Path("orders.csv").open("w", encoding="utf-8") as f:
f.write("id,amount\n1001,250000\n1002,99000\n")
with Path("orders.csv").open(encoding="utf-8") as f:
total = sum(int(line.split(",")[1]) for line in f if not line.startswith("id"))
print(total) # 349000
Hinweis: Gib immer
encoding="utf-8"an. Wenn du das weglässt, verwendet Python die Standardkodierung des Systems. Unter Windows ist das normalerweisecp1252, was zu Problemen mit vietnamesischen Daten führt.
6. Verzeichnisse prüfen und durchsuchen
6.1. Status prüfen
from pathlib import Path
p = Path("orders.csv")
print(p.exists(), p.is_file(), p.is_dir())
print(p.stat().st_size)
print(p.resolve())
True True False
36
/home/minh/project/orders.csv
resolve() gibt den absoluten Pfad zurück und löst Symlinks auf. Es ersetzt os.path.abspath und os.path.realpath.
6.2. Dateien auflisten und suchen
from pathlib import Path
root = Path("shop")
(root / "orders" / "2026").mkdir(parents=True, exist_ok=True)
(root / "orders" / "2026" / "o1.json").write_text("{}", encoding="utf-8")
(root / "orders" / "o0.json").write_text("{}", encoding="utf-8")
(root / "readme.md").write_text("# Shop", encoding="utf-8")
print(sorted(p.name for p in root.iterdir()))
print(sorted(str(p) for p in root.glob("*.md")))
print(sorted(str(p) for p in root.rglob("*.json")))
['orders', 'readme.md']
['shop/readme.md']
['shop/orders/2026/o1.json', 'shop/orders/o0.json']
iterdir()ersetztos.listdir(). Es gibt vollständigePath-Objekte zurück, nicht nur Dateinamen, sodass du nicht wieder verbinden musst.glob("*.md")sucht nur im aktuellen Verzeichnis.rglob("*.json")sucht rekursiv, äquivalent zuglob("**/*.json").
Ab Python 3.12 kann Path.walk() os.walk() ersetzen:
for dirpath, dirnames, filenames in Path("shop").walk():
print(dirpath, filenames)
7. Erstellen, Umbenennen und Löschen
import shutil
from pathlib import Path
backup = Path("backup/daily")
backup.mkdir(parents=True, exist_ok=True)
src = Path("orders.csv")
dst = src.rename(backup / "orders_2026-10-17.csv")
print(dst)
dst.unlink()
Path("not_there.txt").unlink(missing_ok=True)
backup.rmdir()
shutil.rmtree("backup")
backup/daily/orders_2026-10-17.csv
mkdir(parents=True, exist_ok=True)entsprichtos.makedirs(..., exist_ok=True).renamegibt einen neuenPathzurück. Unter Windows gibtrenameeinen Fehler aus, wenn die Zieldatei bereits existiert. Um auf allen Plattformen zu überschreiben, verwendest dureplace.unlinklöscht die Datei. Mitmissing_ok=Truegibt es keinen Fehler, wenn die Datei nicht existiert.rmdirlöscht nur leere Verzeichnisse. Um ein ganzes Verzeichnisbaum zu löschen, brauchst du immer nochshutil.rmtree. Diese Funktion akzeptiertPathganz normal.
8. Schnellreferenztabelle
| os / os.path | pathlib |
|---|---|
os.path.join(a, b) |
Path(a) / b |
os.path.basename(p) |
p.name |
os.path.dirname(p) |
p.parent |
os.path.splitext(p) |
p.stem, p.suffix |
os.path.exists(p) |
p.exists() |
os.path.isfile(p) / isdir |
p.is_file() / p.is_dir() |
os.path.abspath(p) |
p.resolve() oder p.absolute() |
os.path.expanduser("~") |
Path.home() oder p.expanduser() |
os.getcwd() |
Path.cwd() |
os.listdir(p) |
p.iterdir() |
glob.glob("**/*.py", recursive=True) |
p.rglob("*.py") |
os.makedirs(p, exist_ok=True) |
p.mkdir(parents=True, exist_ok=True) |
os.remove(p) |
p.unlink() |
os.rename(a, b) |
a.rename(b) |
os.path.getsize(p) |
p.stat().st_size |
os.path.relpath(p, start) |
p.relative_to(start) |
9. Tests mit pytest schreiben
Das pytest-Fixture tmp_path gibt bereits ein Path-Objekt zurück, daher ist Test-Code mit pathlib sehr prägnant. Angenommen, du hast eine Funktion, die PDF-Rechnungsdateien in ein Archivverzeichnis verschiebt:
# archive.py
from pathlib import Path
def archive_invoices(source: Path, target: Path) -> list[Path]:
target.mkdir(parents=True, exist_ok=True)
moved = []
for pdf in sorted(source.rglob("*.pdf")):
moved.append(pdf.rename(target / pdf.name))
return moved
Der entsprechende Test:
# test_archive.py
from pathlib import Path
from archive import archive_invoices
def test_archive_moves_only_pdf_files(tmp_path: Path):
source = tmp_path / "inbox"
(source / "2026").mkdir(parents=True)
(source / "a.pdf").write_bytes(b"%PDF")
(source / "2026" / "b.pdf").write_bytes(b"%PDF")
(source / "note.txt").write_text("skip me", encoding="utf-8")
target = tmp_path / "archive"
moved = archive_invoices(source, target)
assert [p.name for p in moved] == ["b.pdf", "a.pdf"]
assert sorted(p.name for p in target.iterdir()) == ["a.pdf", "b.pdf"]
assert (source / "note.txt").exists()
assert not (source / "a.pdf").exists()
pytest -q test_archive.py
. [100%]
1 passed in 0.02s
Die Reihenfolge in moved ist b.pdf vor a.pdf. Grund: sorted vergleicht Path-Objekte, und inbox/2026/b.pdf kommt vor inbox/a.pdf, weil das Zeichen 2 vor a kommt. Dieses Detail wird oft übersehen, wenn du von String-Vergleich zu Path-Vergleich wechselst.
10. Häufige Fehler
Ein Path mit einem String addieren. Path("data") + "/x.txt" gibt einen TypeError. Verwende / statt +. Wenn du wirklich an den Dateinamen anhängen möchtest, verwendest du p.with_name(p.name + "_old").
Die rechte Seite ist ein absoluter Pfad. Das Ergebnis könnte überraschend sein:
from pathlib import Path
print(Path("/srv/uploads") / "/etc/passwd") # /etc/passwd
Die absolute Komponente auf der rechten Seite ersetzt den gesamten linken Teil. Wenn der Dateiname von einem Benutzer kommt, musst du es vorher überprüfen:
base = Path("/srv/uploads").resolve()
candidate = (base / user_input).resolve()
if not candidate.is_relative_to(base):
raise ValueError("Ungültiger Pfad")
with_suffix vermisst den Punkt. p.with_suffix("pdf") gibt einen ValueError. Du musst ".pdf" schreiben.
Vergessen, dass glob einen Generator zurückgibt. Du kannst ihn nur einmal durchlaufen. Wenn du ihn erneut verwenden oder len() aufrufen möchtest, wickle ihn in list(...) ein. Die Reihenfolge der Ergebnisse ist auch nicht garantiert, also brauchst du sorted, wenn die Reihenfolge wichtig ist.
Verborgene Dateien und stem. Mit Path(".env") ist suffix ein leerer String und stem ist ".env". Nimm nicht an, dass alle Dateien eine Erweiterung haben.
Alte Bibliotheken akzeptieren nur str. Die meisten Standard-APIs akzeptieren os.PathLike seit Python 3.6. Wenn du auf eine alte Bibliothek triffst, verwendest du str(p) oder os.fspath(p).
Hinweis:
pathlibersetztosundshutilnicht vollständig. Um Dateien zu kopieren, verwendest du immer nochshutil.copy2(Python 3.14 fügtPath.copyhinzu). Umgebungsvariablen werden immer noch überos.environgelesen. Komplexe Zugriffskontrolle erfordert immer nochos.chmododeros.access.
Wann ist eine Konvertierung nicht nötig? Bei Schleifen, die Millionen von Pfad-Strings verarbeiten, ohne auf die Festplatte zuzugreifen, ist die Erstellung von Path-Objekten langsamer als reine String-Operationen. Dies ist selten, und du solltest messen, bevor du optimierst. Bei altem Code, der stabil läuft, musst du nicht alles auf einmal ändern. Konvertiere schrittweise, wenn du ein Modul anfasst.
11. Fazit
pathlib bündelt die über os, os.path und glob verteilten Operationen in einem einzigen Objekt. Der Code ist kürzer, lesbarer und weniger fehleranfällig für String-Verkettungen. Du hast gesehen, wie man Pfade mit / verbindet, Komponenten mit name, stem, suffix, parent extrahiert, Dateien mit read_text und write_text liest und schreibt, Verzeichnisse mit iterdir, glob, rglob, walk durchsucht und Tests mit pytest’s tmp_path schreibt.
Du solltest dir drei Punkte merken. Übergib immer encoding. Sei vorsichtig beim Verbinden mit absoluten Pfaden aus externen Eingaben. Verwende immer noch shutil für Operationen wie Kopieren und Löschen ganzer Verzeichnisbäume. Bei neuem Code verwende standardmäßig Path und wende dich nur os.path zu, wenn du einen konkreten Grund hast.