In diesem Kapitel wollen wir (Nutzer-)Input und Dateien lesen und schreiben mit den Modulen sys, os, os.path und pathlib (und etwas shutil).
Damit haben wir dann die rudimentären Werkzeuge an der Hand, um Python für sogenannte Shell-Skripte zu verwenden. Um eine Bibliothek, d.h. Code, der zur Verwendung in anderem Code gedacht ist, von einem Shell-Skript oder einer Anwendung zu unterscheiden, können wir folgendes vereinbaren:
Eine Anwendung enthält nur wenig imperativen Code ohne Einrückungen, sondern im Wesentlichen nur am Ende der Datei, die aufgerufen wird, um die Anwendung zu starten (main.py o.ä.) den Block
if __name__ == "__main__":
main()
Die eigentliche main-Methode muss dann natürlich zuvor definiert werden. Der Name der Methode und der Datei sind veränderlich, aber "__main__" ist eine feststehende Konstante.
Code, der keine solche Anweisung enthält, ist typischerweise eine Bibliothek, dazu gedacht mit import importiert zu werden, oder ein kurzes Skript.
Achtung: einige Bibliotheken sind gleichzeitig als Anwendungen geschrieben, sodass sowohl python -m module als auch import module sinnvolle Verwendungen sind.
In diesem Kapitel begegnen wir mehrmals dem Phänomen, dass sich eine Aufgabe entweder mit den Modulen os und sys lösen lässt, oder aber spezifischere, ‘bessere’ Module zum Einsatz kommen können. Ein hilfreiches Muster, um zu entscheiden, welche dieser beiden Optionen man in jedem Entscheidungsfall wählt, ist zunächst für ein kleines Skript die ‘schlechtere’ Version zu verwenden (die ggf. weniger Exception-Handling etc. bietet) und nach Anwachsen des Codes, beim Refactoring, dann auf die ‘bessere’ aber teilweise auch komplexere Lösung umzusteigen. Mit zunehmender Erfahrung lernt man, einzuschätzen wo welche Bibliotheken von vornherein Arbeit sparen oder andere Vorteile bringen und an welchen Stellen (für Skripte, Single-Use-Code, Rapid Prototyping) quick&dirty geboten ist.
Sys¶

Das Modul sys vereint Variablen und Methoden, die in Java am ehesten in java.lang.System und java.lang.Runtime zu finden sind.
Die Variable sys.platform hat z.B. den Wert linux, win32 oder darwin und kann für Verzweigung zu Betriebssystem-spezifischem Code verwendet werden.
argv¶
Wenn wir mit python filename.py Hello das folgende, unter filename.py gespeicherte Skript aufrufen (oder das Skript ausführbar gemacht haben, unter Linux mit chmod u+x filename.py, und dann ./filename.py Hello aufrufen), so werden die Argumente in die Liste sys.argv verpackt. Dabei ist das erste Element, mit Index 0, stets der Name des Skripts. Man kann daraus also den Kontext, wie der Code aufgerufen wurde, entnehmen. Im Interpreter ist sys.argv[0] leer.
import sys
print(sys.argv[0]) # Output: filename.py
print(sys.argv[1]) # Output: Hello
In der Praxis wird sys.argv kaum genutzt, weil das Modul argparse es erheblich leichter macht, (Nutzer)schnittstellen über Kommandozeilenparameter zu definieren, siehe den Abschnitt zu argparse. Das wesentlich ältere optparse sollte man nicht mehr verwenden.
path und modules¶
Wenn man mit import x das Modul x neu importiert, sucht Python nach x im sys.path, das ist also das Analogon von Java’s Classpath.
Wenn man dynamisch neue Orte für Modulimporte hinzufügen möchte, z.B. weil man dynamisch Code erzeugt hat oder Plugins aus dem Internet heruntergeladen hat, kann man einfach die Liste sys.path ergänzen zur Laufzeit.
Es kann hilfreich zum Debugging sein, wenn man einen Überblick gewinnt, wo im System überall Python Module installiert sind und welche dieser Orte tatsächlich für den Import verwendet wird.
Für ein konkretes Modul x, welches nicht built-in ist wie sys, z.B. numpy kann man auch konkret abfragen, welche Datei für den Import ausgewählt wurde mit x.__file__.
Der gerade laufende Python-Interpreter ist die Datei mit Pfad sys.executable, die Version sys.version.
Wenn man import x aufruft, sucht Python zunächst im Modul-Cache sys.modules, ob es bereits importiert wurde und nutzt ggf. diese Version. Um manuell Module neu zu laden (z.B. weil man gerade den Code geändert hat), kann man importlib.reload verwenden; das ist aber nur selten nützlich.
Exit¶
Um das Programm bzw. den Interpreter zu beenden, kann man sys.exit(status) aufrufen, wobei status ein Integer ist, der den Exit Code angibt. Der Code 0 steht für Erfolg, alle anderen sind Fehlercodes. Der Aufruf von sys.exit wirft eine SystemExit-Exception, die man sogar wieder auffangen kann:
import sys
try:
sys.exit(1)
except SystemExit:
_ = sys.stdout.write("We just go on")
finally:
_ = sys.stdout.write(" and on...\n")
# Output: We just go on and on...
Die Zuweisung _ = erfüllt hier den Zweck, dass ansonsten der Rückggabewert von sys.stdout.write in einer interaktiven Umgebung, wie einer REPL oder einem Notebook, angezeigt werden würde (die Anzahl geschriebener Bytes). Der Variablenname _ kommuniziert, dass der Wert nicht wirklich verwendet werden soll.
Standard I/O¶
Die Entsprechung von Java’s System.in, System.out, System.err sind in Python sys.stdin, sys.stdout, sys.stderr.
Die print-Methode ruft intern sys.stdout.write auf, hängt allerdings an den Eingabestring stets einen Zeilenumbruch "\n". Wenn man das vermeiden möchte beim Ausgeben von Text, kann man print(string, end="") verwenden. Für sehr viel direkte Kontrolle kann man auch sys.stdout.write direkt verwenden. Um z.B. auf den Error-Stream zu schreiben, sollte man typischerweise print(string, file=sys.stderr) benutzen. Wir werden im Kapitel über Logging noch lernen, wie man systematischer Logging auf Output-Streams und in Dateien verwendet.
Um schnell Nutzereingaben einzulesen, etwa für ein interaktives Skript, gibt es die Methode input.
Os (und pathlib)¶

Während sys eher für den Interpreter zuständig ist, ist os die Schnittstelle zum Betriebssystem und Dateisystem. So hat z.B. os.name etwa die Werte nt für Windows oder posix für Linux.
Environment¶
Die Variable os.environ enthält wie ein dict die Umgebungsvariablen. Ein übliches Muster ist, über Umgebungsvariablen Credentials (Anmeldungsinformationen) einzulesen, damit diese nicht zusammen mit dem Code versioniert werden:
import os
try:
username = os.environ["DB_USER"]
password = os.environ["DB_PASS"]
except KeyError as e:
print("Could not find in environment:", e)
print("Now we have it:", username, password)
Man kann auch direkt Umgebungsvariablen zuweisen, also etwa os.environ["DB_USER"] = "Nutzername". Dabei verhält sich os.environ wie ein dict, aber das ist nur ein Duck Type, denn implizit werden hier Getter und Setter aufgerufen, die mit der Betriebssystemumgebung interagieren. Die Setter führen z.B. einen Type Check durch, denn Umgebungsvariablen können nur Strings sein.
Warnung: Umgebungsvariablen sind in Linux case-sensitive, in Windows nicht. Unter Windows führt daher auch os.environ["db_user"] zur gleichen Umgebungsvariable wie os.environ["DB_USER"].
Eine sehr nützliche Umgebungsvariable, wenn man externe Programme aufrufen möchte, ist PATH. Fügt man der Umgebung etwas hinzu, z.B. durch abändern des PATH oder hinzufügen von Credentials, so wird diese Umgebung an aufgerufene Unterprozesse weitergegeben.
Ein häufiges Pattern ist auch, wie bei einem Dictionary üblich, einen Default-Wert zu verwenden, wenn die Umgebungsvariable nicht gesetzt ist (silent fail / soft fail):
import os
API_KEY = os.environ.get("API_KEY", None)
Um tatsächlich Credentials aus einer Datei in Umgebungsvariablen einzulesen kann man python-dotenv verwenden - das ist ein Beispiel dafür, wie für typische Workflows und wiederkehrende Probleme häufig ein Paket im Python Package Index (PyPI) existiert, welches dieses Problem löst bzw. den Workflow abbildet.
Working directory¶
Anders als in Java, kann man in Python das Arbeitsverzeichnis (current working directory = cwd) mit os.chdir ändern:
import os
print(os.getcwd()) # Output: '/home/voelkel'
os.chdir("../")
print(os.getcwd()) # Output: '/home'
Arbeiten mit Ordnern¶
Gegeben einen String folder, der den Pfad zu einem Ordner enthält, können wir mit os.listdir(folder) eine Liste bekommen, die die Namen der Inhalte (Ordner und Dateien) enthält. Mit os.mkdir(folder) erstellt man Ordner, mit os.rmdir(folder) entfernt man leere Ordner.
Wenn man auf einem Ordner folder welcher nicht leer ist, os.rmdir(folder) aufruft, wird ein OSError geworfen. Es gibt keine spezifischere Exception, aber man kann den Fehlercode auch direkt abrufen:
import os
import errno
path1 = "/tmp/soontobegone"
path2 = path1+"/nonempty"
try:
os.rmdir(path1)
except OSError as e:
if e.errno == errno.ENOTEMPTY or e.winerror == 145:
print(path1, "is not empty") # Output: /tmp/soontobegone is not empty
else:
raise
os.rmdir(path2)
os.rmdir(path1)
Um rekursiv Ordner zu löschen (beginnend mit den Blättern des Verzeichnisbaums) gibt es z.B. shutil.rmtree.
Erstellt man hingegen einen Ordner mit os.mkdir(folder) obwohl folder bereits existiert, so wird ein FileExistsError geworfen, den man also auffangen kann. Alternativ lässt sich os.mkdirs (zur Erstellung mehrerer Ordner auf einmal) das Schlüsselwort-Argument exists_ok=True übergeben und damit werden keine Fehler geworfen, wenn ein Ordner bereits existiert (nur, wenn anstelle eines Ordners eine Datei vorgefunden wird). Um zu sehen, ob der Ordner (oder die Datei!) folder existiert, ruft man os.path.exists(folder) auf. Um zu sehen, ob es sich um einen Ordner handelt, os.path.isdir(folder). Entsprechend gibt os.path.isfile(filename) an, ob es sich bei dem String filename um den Pfad einer Datei handelt.
Pathlib¶
Wenn man etwas intensiver mit Pfaden und Ordnern arbeitet, ist es unpraktisch, Pfade als Strings zu repräsentieren; es gibt daher mit dem Modul pathlib einen objektorientierten Zugang. Code, der viel os.path verwendet, stammt oft noch aus einer Zeit, als es pathlib nicht gab.
Während man in Python früher Pfade sicher konkateniert hat mit os.path.join, so gibt es in pathlib einen überschriebenen Divisons-Operator, der syntaktisch etwas schöner ist:
import os
myPath = os.path.join("/", "tmp", "soontobegone", "filename.tar.gz")
filename = os.path.basename(myPath)
stem, suffix = os.path.splitext(filename)
print(myPath, filename, stem, suffix)
# Output: /tmp/soontobegone/filename.tar.gz filename.tar.gz filename.tar .gz
from pathlib import Path
myPathObject = Path("/") / "tmp" / "soontobegone" / "filename.tar.gz"
print(myPathObject, myPathObject.name, myPathObject.stem, myPathObject.suffix)
# Output: /tmp/soontobegone/filename.tar.gz filename.tar.gz filename.tar .gz
Man kann mit myPathObject.write_text oder auch myPathObject.write_bytes und den entsprechenden Leseoperationen arbeiten, aber diese sind nicht so flexibel wie mit myPathObject.open ein TextIOWrapper-Objekt zu verwenden. Um kleine Dateien wie z.B. Konfigurationsoptionen zu lesen, ist es aber besser, weil weniger Code zu schreiben/lesen ist.
Ein pathlib.Path kann auch verwendet werden in os.listdir (und letztlich jedes Objekt, welches eine __fspath__-Methode implementiert, dank Duck Typing).
Seit Python 3.12 kann man anstelle von os.walk auch direkt myPathObject.walk aufrufen:
from pathlib import Path
root = Path("/tmp")
for dirpath, dirnames, filenames in root.walk():
for file in filenames:
print(dirpath / file)
Bis Python 3.11 erhalten wir dabei allerdings einen AttributeError, denn object has no attribute 'walk'. So sieht der entsprechende Code mit os.walk aus:
import os
from pathlib import Path
root = "/tmp"
for dirpath, dirnames, filenames in os.walk(root):
for file in filenames:
print(Path(dirpath) / file)
Der walk geht tatsächlich rekursiv über alle Unterordner und emittiert ein Tupel (dirpath, dirnames, filenames) pro Unterordner.
Alternativ kann man auch mit Wildcards, sogenannten Glob-Mustern arbeiten:
from pathlib import Path
# matcht txt Dateien die genau 2 Ordner tiefer liegen
for filename in Path(".").glob("*/*/*.txt"):
print(filename)
Sowohl glob, walk, listdir als auch Path.iterdir sind wrapper um os.scandir, welches man bei performancekritischen Anwendungen ggf. auch direkt verwendet, da es den Overhead von Python Objekten (pathlib.Path Instanzen) vermeidet.
Arbeiten mit Dateien¶
Dateien zu verschieben ist das selbe wie ihren Pfad umzubenennen, daher entspricht os.rename(src, dst) dem Linux-Befehl mv src dst. Dateien löschen kann man mit os.remove(path).
Um die Rechte einer Datei zu ändern, kann man os.chmod(path, mode) verwenden. Die üblichen Daten wie Größe, Erstellungszeit, Modifikationszeit, Besitzer-Username erhält man mit os.stat(path).
Weil z.B. os.rename(src, dst) fehlschlägt, wenn src und dst nicht auf dem gleichen Dateisystem liegen, sollte man statt solcher low-level-Befehle in der Regel das Modul shutil nutzen:
import shutil
pathA = "/tmp/somefile.txt"
pathB = "/tmp/anothername.txt"
# assume file at pathA exists
shutil.move(pathA, pathB)
shutil.copy(pathB, pathA)
shutil.remove(pathB)Ansonsten sind auch shutil.disk_usage(path) sowie shutil.unpack_archive sowie shutil.make_archive recht nützlich.
Die übliche Methode, Dateien einzulesen, ist mit open(path). Diese Methode gibt einen TextIOWrapper zurück, den wir verwenden können und abschließend schließen sollten:
path = "/tmp/soontobegone.txt"
f = open(path, mode='w', encoding='utf-8')
f.write("content")
f.close()
try:
g = open(path, encoding='utf-8') # default mode='r'
content = g.read()
except FileNotFoundError as e:
content = None
finally:
g.close()
print(content) # Output: content
In früheren Python-Versionen wurde locale.getpreferredencoding() verwendet als Fallback, wenn kein encoding= übergeben wurde. Mit Python 3.15 soll endgültig UTF-8 als Standardencoding verwendet werden, unabhängig vom System. Es wird empfohlen, stets das Encoding mit anzugeben.
Wenn man eine Datei einläd, die nicht gescheit aussieht (wegen falschen Encodings) kann man gut anstelle von 'utf-8'' kurz 'latin-1' sowie 'cp1252' ausprobieren. Bei wiederkehrenden Problem, wenn man z.B. eine ganze Menge älterer Dateien unterschiedlichen Encodings einlesen möchte, kann die Bibliothek chardet dabei helfen - darin sind Heuristiken implementiert, die das verwendete Encoding schätzen.
Statt explizit auf einer Datei f mit f.read() oder so zu arbeiten, kann man die Datei auch als Iterable verwenden, wobei über die Zeilen iteriert wird mit for line in f:.
Da der try-open-except-finally-close-Tanz immer derselbe ist, verwendet man in Python einen Kontext-Manager mit dem Schlüsselwort with um das close() automatisch geschehen zu lassen (siehe den Abschnitt zu try und with):
with open(path, encoding='utf-8') as f:
content = "Found "
for line in f:
content += line
print(content) # Output: Found content
Ein vom Encoding unabhängiges aber verwandtes Problem ist das Zeilenende. In os.linesep ist gespeichert, was das aktuelle Betriebssystem verwendet - unter Linux ist das in der Regel '\n'.
Ein weiteres Problem unter Windows ist, dass der Backslash '\' in Pfaden genutzt wird; wenn wir einen String mit Backslashes in Python-Code schreiben wollen, können wir mit path = r"C:\Verzeichnis" arbeiten. Das Ergebnis von r"..." ist ein normaler String, aber beim Einlesen werden keine Escape-Sequenzen verarbeitet. Das r steht für raw/roh.
Wenn man nicht auf der Ebene von Strings arbeiten möchte, sondern mit Binärdaten, so ist kein Encoding erforderlich (denn es wird nichts dekodiert), sondern es wird als mode-Parameter bei open statt 'r' bzw. 'w' mit 'rb' bzw. 'wb' gearbeitet.
Arbeiten mit Prozessen¶
In älterem Code findet man noch Aufrufe von os.system um neue Prozesse (externe Programme) zu starten, in neuem Code sollte man dazu das subprocess-Modul verwenden, was wir im Abschnitt über Prozesse noch kennenlernen.
Wenn es nur darum geht, schnell ein Skript ähnlich eines Bash-Scripts (oder Windows PowerShell oder Batchfile etc.) zu schreiben, kann man mit os.system(command) das gleiche bewirken, wie in der Shell direkt den Inhalt vom String command einzugeben (mitsamt Parametern). Dabei gibt os.system nur den Exit Code zurück, nicht die Ausgabe des Programms.
Nützlich kann es sein, etwa mit os.getpid() zu sehen, welche ID der aktuelle Python-Interpreter-Prozess hat (etwa, um das Inspektionswerkzeug py-spy einzusetzen).
Wenn man für aufwändige Rechnungen mehrere Prozessoren nutzen möchte, gibt os.cpu_count() die Obergrenze an.