PCPP1 Block 5 (Teil 2b): configparser, os, datetime, time, io
Track Python · PCPP1 Block 5, Ziele 5.5 und 5.6 · ca. 45 Min.
Worum es geht
Block 5 “Dateiverarbeitung und Umgebung” hat laut Quelle 15 % der PCPP1-Prüfung, das sind 7 Fragen (Stand der Quelle: 1. Oktober 2026, bitte vor der Buchung prüfen). Diese Lektion ist Teil 2b und deckt configparser (Konfigurationsdateien, Ziel 5.5) sowie os, datetime, time und io (die Programmumgebung, program environment, Ziel 5.6) ab.
Was du aus Teil a brauchst: Nichts Fachliches. Teil a (Teil 2a: logging) behandelt die Logs. Hier kommt nur eine Gemeinsamkeit vor: Die Programmumgebung liefert Text, und dein Code macht daraus Typen.
Praxisbezug für dich als Freelancer: Fast jede Anwendung liest Einstellungen aus einer Datei oder aus Umgebungsvariablen (environment variables). Und Fristen, Zeitzonen und Dauern sind eine der häufigsten Fehlerquellen im Alltag.
Nicht in dieser Lektion: pathlib (nur der Name kommt vor). Alles hier läuft im Browser: Konfiguration kommt aus Strings (read_string), Dateien nur in temporären Ordnern.
Von JS/TS her gedacht
| Thema | JS/TS | Python |
|---|---|---|
| Einstellungen | .env plus process.env.PORT (immer String) |
.ini plus configparser, os.environ["PORT"] (immer String) |
| Datum | new Date(), Date.now(), Bibliotheken wie date-fns |
datetime, timedelta, strftime und strptime |
| Dauer messen | performance.now() |
time.perf_counter() |
| Text wie eine Datei | new Blob, Streams |
io.StringIO, io.BytesIO |
In Node steckst du die Konfiguration oft so zusammen:
const port = Number(process.env.PORT ?? "8080"); // alles ist String, du wandelst selbst um
const debug = ["1", "true", "yes"].includes((process.env.DEBUG ?? "").toLowerCase());configparser nimmt dir genau diese Umwandlung ab (getint, getboolean). Sonst ist der Gedanke derselbe: Eine Datei oder Umgebung liefert Text, dein Code macht daraus Typen.
Konzept in kleinen Schritten
Schritt 1: configparser (Ziel 5.5)
Eine .ini-Datei hat Abschnitte in eckigen Klammern und key = value-Zeilen. Der Abschnitt [DEFAULT] liefert Standardwerte für alle anderen. Hier aus einem String statt einer Datei (read_string):
Ausgabe:
['database', 'app']
localhost 5432 True
postgres://localhost:5432/app
30
x
10%
54321
[]
Beobachtungen:
- Alle Werte sind Strings.
cfg["database"]["port"] + "1"ergibt"54321", keine Rechnung. Umwandeln mitgetint,getfloat,getboolean(verstehtyes/no,on/off,true/false,1/0). - Interpolation: Standard ist
%(name)s. Ein echtes Prozentzeichen schreibst du als%%(10%%wird zu10%). MitExtendedInterpolationgilt${abschnitt:schluessel}. [DEFAULT]gilt für alle Abschnitte:timeoutist auch in[app]sichtbar.- Schlüssel werden kleingeschrieben, Abschnittsnamen sind case-sensitive.
read()auf eine fehlende Datei wirft keinen Fehler. Es gibt nur die Liste der gelesenen Dateien zurück, hier[]. Wer den Rückgabewert nicht prüft, arbeitet mit einer leeren Konfiguration.cfg["x"]für einen fehlenden Abschnitt wirftKeyError,getwirftconfigparser.NoSectionErroroderNoOptionError. Mitfallback=bekommst du stattdessen einen Standardwert.- Weitere Methoden:
has_section,has_option,options,items,read_dict,add_section,remove_optionundwrite(f).
Schritt 2: os, datetime, time, io (Ziel 5.6)
| Modul | Wichtige Funktionen |
|---|---|
os |
getcwd, chdir, listdir, makedirs(p, exist_ok=True), remove, rename, walk, environ, getenv("X", default), cpu_count, getpid, path.join, path.exists, path.splitext |
datetime |
datetime.now(), date.today(), timedelta, strftime, strptime, isoformat, fromisoformat, timezone.utc |
time |
time() (Zeitstempel), sleep, perf_counter() (Dauer), monotonic, localtime, gmtime, strftime |
io |
StringIO, BytesIO, open, Basisklassen IOBase, RawIOBase, BufferedIOBase, TextIOBase |
Zuerst datetime, die Differenz zweier Zeitpunkte ist ein timedelta:
Ausgabe:
01.10.2026 09:30
2026-10-31
83 days, 14:30:00 83 7223400.0
-84 34200
TypeError: can't subtract offset-naive and offset-aware datetimes
2026-10-01T09:30:00+00:00 2026-10-01T12:30:00+03:00
Der Rest ist 83 days, 14:30:00. .days zählt nur die ganzen Tage (83), den Rest unter einem Tag hält .seconds, und total_seconds() rechnet alles in Sekunden um. Bei negativen Differenzen rechnet timedelta anders, als man denkt: -84 days plus 34200 Sekunden. Wer für “abgelaufen” .days benutzt, bekommt bei 23 Stunden Verspätung -1. Ein naives datetime (ohne Zeitzone) und ein aware datetime (mit Zeitzone) lassen sich nicht vergleichen oder subtrahieren (TypeError).
Format-Codes: %Y Jahr, %m Monat, %d Tag, %H Stunde (24 h), %M Minute, %S Sekunde, %A Wochentag, %B Monatsname, %j Tag des Jahres, %f Mikrosekunden. Für Zeitmessung: time.perf_counter() für Dauern, time.time() für Zeitstempel.
Jetzt io, os und time. Dateien legst du hier in einem temporären Ordner an, der danach verschwindet:
Ausgabe:
'a\n' 'b\n' ''
'a\nb\n'
b'ab' 2
['b'] ('bericht.tar', '.gz')
FileExistsError ohne exist_ok
dflt None
5 <class 'str'>
True
Umgebungsvariablen sind immer Strings (wie process.env in Node). os.getenv("X", default) und os.environ.get("X") geben bei fehlender Variable den Standardwert oder None. os.environ["X"] wirft dann einen KeyError.
Falle: die Prüfungs- und Praxisklassiker
configparser: Werte sind Strings, undreadauf eine fehlende Datei wirft keinen Fehler.- Naive und aware
datetimemischen ergibt einenTypeError. timedelta.daysbei negativen Differenzen ist-1schon bei einer Stunde Verspätung. Für “abgelaufen” vergleichst du Zeitpunkte oder nutzttotal_seconds().
Übungen
Übung 1: Einstellungen aus einem INI-Text laden (ca. 12 Min.)
Schreibe lade_einstellungen(text). Der Text ist der Inhalt einer INI-Datei, du liest ihn mit read_string. Rückgabe ist ein Dict mit diesen Schlüsseln und Typen:
| Schlüssel | Quelle | Typ | Wenn es fehlt |
|---|---|---|---|
host |
Abschnitt server, Option host |
str |
(muss da sein) |
port |
server, port |
int |
80 |
url |
server, url (mit Interpolation) |
str |
"" |
timeout |
server, timeout (auch aus [DEFAULT]) |
float |
30.0 |
debug |
Abschnitt app, debug |
bool |
False (auch wenn der Abschnitt app fehlt) |
motd |
server, motd |
str |
"" |
Gibt es keinen Abschnitt server (Abschnittsnamen unterscheiden Groß- und Kleinschreibung), gibt die Funktion None zurück.
Welche Methode liest aus einem String statt aus einer Datei, und was passiert, wenn du stattdessen die Methode für Dateien mit dem Text fütterst? Für jeden Typ gibt es eine passende get...-Methode, und alle akzeptieren ein Schlüsselwort für den Ersatzwert. Wie prüfst du, ob ein Abschnitt existiert, bevor du hineingreifst?
import configparser
def lade_einstellungen(text):
cfg = configparser.ConfigParser()
cfg.read_string(text)
if not cfg.has_section("server"):
return None
return {
"host": cfg.get("server", "host"),
"port": cfg.getint("server", "port", fallback=80),
"url": cfg.get("server", "url", fallback=""),
"timeout": cfg.getfloat("server", "timeout", fallback=30.0),
"debug": cfg.getboolean("app", "debug", fallback=False),
"motd": cfg.get("server", "motd", fallback=""),
}
lade_einstellungenread_string liest den Text selbst, read würde den Text als Dateinamen behandeln und eine fehlende Datei still ignorieren. getint, getfloat und getboolean wandeln um (getboolean versteht yes/no, on/off, 1/0). fallback= greift auch dann, wenn der ganze Abschnitt fehlt. [DEFAULT] und die Interpolation (%(host)s, %%) erledigt ConfigParser beim Lesen.
Übung 2: Fristen mit datetime berechnen (ca. 10 Min.)
Schreibe frist_info(jetzt, frist_text). jetzt ist ein datetime (naiv oder aware), frist_text hat das Format "TT.MM.JJJJ HH:MM", zum Beispiel "24.12.2026 18:00".
- Ist
jetztaware, gilt die Frist in derselben Zeitzone wiejetzt. Istjetztnaiv, bleibt die Frist naiv. - Rückgabe: Dict mit
"abgelaufen"(True, wenn die Frist vorjetztliegt, bei genau gleicher ZeitFalse),"tage"(volle Tage Restzeit),"stunden"(übrige volle Stunden, 0 bis 23) und"iso"(die Frist alsisoformat()). - Ist die Frist abgelaufen, sind
tageundstundenbeide0. - Hat der Text ein falsches Format, soll ein
ValueErrorentstehen (nicht abfangen).
Zerlege es: Text in einen Zeitpunkt umwandeln (welcher Aufruf, welches Format?), dann entscheiden, ob dem Zeitpunkt eine Zeitzone fehlt, die jetzt hat. Die Differenz ist ein timedelta. Zwei Attribute davon liefern Tage und den Rest in Sekunden. Was liefert .days bei einer negativen Differenz, und warum prüfst du “abgelaufen” besser vorher?
from datetime import datetime, timedelta, timezone
def frist_info(jetzt, frist_text):
frist = datetime.strptime(frist_text, "%d.%m.%Y %H:%M")
if jetzt.tzinfo is not None:
frist = frist.replace(tzinfo=jetzt.tzinfo)
rest = frist - jetzt
abgelaufen = rest.total_seconds() < 0
tage = stunden = 0
if not abgelaufen:
tage = rest.days
stunden = rest.seconds // 3600
return {"abgelaufen": abgelaufen, "tage": tage, "stunden": stunden, "iso": frist.isoformat()}
frist_infostrptime wandelt Text mit Formatcodes in ein datetime und wirft bei falschem Format von selbst einen ValueError. replace(tzinfo=...) macht die naive Frist aware, sonst gäbe die Subtraktion einen TypeError. rest.days zählt volle Tage, rest.seconds den Rest unter einem Tag. Bei negativen Differenzen zählt timedelta anders (-1 days plus Sekunden), darum wird “abgelaufen” zuerst über total_seconds() entschieden.
Übung 3: Puffer und Umgebung vorhersagen (ca. 8 Min.)
Der folgende Code liest aus einem io.StringIO-Puffer, aus Umgebungsvariablen und zerlegt einen Dateinamen. Er steht fest, du sagst voraus, was in antwort landet. Trage das Tupel mit sechs Werten ein, ohne den Code auszuführen:
import io, os
os.environ["PORT_18"] = "8080"
puffer = io.StringIO("a,b\n1,2\n")
kopf = puffer.readline()
rest = puffer.read()
nochmal = puffer.read()
port = os.getenv("PORT_18")
fehlt = os.getenv("FEHLT_18", "dflt")
name, endung = os.path.splitext("export.tar.gz")
antwort = (kopf, rest, port + "1", fehlt, nochmal, endung)Ein StringIO hat eine Leseposition: readline und read verbrauchen den Text, und ein späteres read bekommt nur noch, was übrig ist. Umgebungsvariablen sind immer Strings, auch wenn sie nach Zahlen aussehen. Und splitext trennt nur an der letzten Stelle.
antwort = ("a,b\n", "1,2\n", "80801", "dflt", "", ".gz")
antwortMerksatz
Aus configparser, os.environ und datetime kommt nur Text herein: Typen (getint, getboolean), Ersatzwerte (fallback) und Zeitzonen (naiv gegen aware) musst du selbst festlegen.
Prüfstein
Dein Dienst liest PORT aus der Umgebung und timeout aus einer INI-Datei. Beim Kunden startet er auf Port 80 statt 8080, und eine Frist wird um einen Tag falsch als “abgelaufen” gemeldet. Nenne je zwei mögliche Ursachen und wie du sie mit den Mitteln aus dieser Lektion ausschließt.
Zurück zu Teil 2a: logging.
Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “PCPP1 Block 5: Dateiverarbeitung und Umgebung”, Unterabschnitte 5.5 (configparser), 5.6 (Programmumgebung) und “Typische Fallen in Block 5”. Die Prüfungsgewichtung (15 %, 7 Fragen) steht dort, vor der Buchung bitte prüfen. Nicht aus der Quelle, sondern allgemeines Python-Wissen (bitte prüfen): das genaue Verhalten von timedelta.days und .seconds bei negativen Differenzen und dass fallback= auch bei fehlendem Abschnitt greift. Das Quell-Beispiel erklärt (parsed - start).days == 83 mit “9:30 Uhr bis 0:00 Uhr ist kein voller Tag”. Genauer: Die Differenz ist 83 Tage und 14:30 Stunden, .days schneidet die Stunden ab (bitte prüfen, ob die Quellenformulierung angepasst werden soll). Alle Ausgaben im Text wurden mit Python 3.13 ausgeführt.