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 mit getint, getfloat, getboolean (versteht yes/no, on/off, true/false, 1/0).
  • Interpolation: Standard ist %(name)s. Ein echtes Prozentzeichen schreibst du als %% (10%% wird zu 10%). Mit ExtendedInterpolation gilt ${abschnitt:schluessel}.
  • [DEFAULT] gilt für alle Abschnitte: timeout ist 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 wirft KeyError, get wirft configparser.NoSectionError oder NoOptionError. Mit fallback= bekommst du stattdessen einen Standardwert.
  • Weitere Methoden: has_section, has_option, options, items, read_dict, add_section, remove_option und write(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

  1. configparser: Werte sind Strings, und read auf eine fehlende Datei wirft keinen Fehler.
  2. Naive und aware datetime mischen ergibt einen TypeError.
  3. timedelta.days bei negativen Differenzen ist -1 schon bei einer Stunde Verspätung. Für “abgelaufen” vergleichst du Zeitpunkte oder nutzt total_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_einstellungen

read_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 jetzt aware, gilt die Frist in derselben Zeitzone wie jetzt. Ist jetzt naiv, bleibt die Frist naiv.
  • Rückgabe: Dict mit "abgelaufen" (True, wenn die Frist vor jetzt liegt, bei genau gleicher Zeit False), "tage" (volle Tage Restzeit), "stunden" (übrige volle Stunden, 0 bis 23) und "iso" (die Frist als isoformat()).
  • Ist die Frist abgelaufen, sind tage und stunden beide 0.
  • Hat der Text ein falsches Format, soll ein ValueError entstehen (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_info

strptime 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")
antwort

Merksatz

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.