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 Path ein PosixPath-Objekt zurück. Auf Windows gibt es ein WindowsPath-Objekt zurück. Du brauchst dich nicht um die konkrete Klasse zu kümmern.
  • Path.home() und Path.cwd() ersetzen os.path.expanduser("~") und os.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:

  • stem und suffix teilen nur am letzten Punkt auf. Bei .tar.gz-Dateien musst du suffixes verwenden.
  • parent entspricht os.path.dirname. parents ist eine Sequenz von übergeordneten Verzeichnissen, die bis zur Wurzel aufsteigt.
  • parts teilt 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_text liest den gesamten Inhalt als String.
  • write_bytes und read_bytes arbeiten ä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 normalerweise cp1252, 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() ersetzt os.listdir(). Es gibt vollständige Path-Objekte zurück, nicht nur Dateinamen, sodass du nicht wieder verbinden musst.
  • glob("*.md") sucht nur im aktuellen Verzeichnis.
  • rglob("*.json") sucht rekursiv, äquivalent zu glob("**/*.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) entspricht os.makedirs(..., exist_ok=True).
  • rename gibt einen neuen Path zurück. Unter Windows gibt rename einen Fehler aus, wenn die Zieldatei bereits existiert. Um auf allen Plattformen zu überschreiben, verwendest du replace.
  • unlink löscht die Datei. Mit missing_ok=True gibt es keinen Fehler, wenn die Datei nicht existiert.
  • rmdir löscht nur leere Verzeichnisse. Um ein ganzes Verzeichnisbaum zu löschen, brauchst du immer noch shutil.rmtree. Diese Funktion akzeptiert Path ganz 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: pathlib ersetzt os und shutil nicht vollständig. Um Dateien zu kopieren, verwendest du immer noch shutil.copy2 (Python 3.14 fügt Path.copy hinzu). Umgebungsvariablen werden immer noch über os.environ gelesen. Komplexe Zugriffskontrolle erfordert immer noch os.chmod oder os.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.

pythonpathlibos.pathfile-iopython3