Aufwärmen · Aufgabe 1 von 7
Aufwärmen mit regulären Ausdrücken, die das Log-grep brauchen wird. Was gibt das aus?
import re
print(re.match(r"(\w+) (\w+)", "ERROR db").group(2))// I4.6 · ca. 40 Min. · Aufbau
Nach dieser Lektion loggen Sie aus eigenen Programmen mit Loggern, Leveln, Handlern und Formattern, und Sie haben loggrep gebaut, ein Kommandozeilenwerkzeug, das Logdateien mit re, logging und argparse durchsucht.
Lektion 6 von 6 in I4 Die Standardbibliothek für echte Programme
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
import re
print(re.match(r"(\w+) (\w+)", "ERROR db").group(2))Vorhersagen · Aufgabe 2 von 7
import logging
logging.info("starting")
logging.warning("disk almost full")Üben · Aufgabe 3 von 7
logging.basicConfig(level=logging.____, stream=sys.stdout, format="%(levelname)s %(message)s")Üben · Aufgabe 4 von 7
import logging
import sys
logging.basicConfig(stream=sys.stdout, format="%(name)s [%(levelname)s] %(message)s")
logging.getLogger("shop.cart").error("empty cart")Üben · Aufgabe 5 von 7
Denksport · Aufgabe 6 von 7
import logging
import sys
logging.basicConfig(stream=sys.stdout, format="root: %(message)s")
log = logging.getLogger("app")
log.addHandler(logging.StreamHandler(sys.stdout))
log.warning("disk full")Anwenden · Aufgabe 7 von 7
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Das Modul loggt über seinen eigenen Logger, shop. basicConfig schickt INFO und höher in einem eigenen Format auf den Bildschirm; stream=sys.stdout macht das Log hier nur als normale Ausgabe sichtbar. Ein zweiter Handler am Root-Logger schreibt Fehler, und nur Fehler, in anderem Format nach errors.log. Die debug-Meldung fällt weg, weil das Level INFO ist.
main.py
import logging
import sys
log = logging.getLogger("shop")
def checkout(cart: dict[str, int]) -> int:
log.debug("cart has %d items", len(cart))
if not cart:
log.warning("empty cart, nothing to do")
return 0
total = sum(cart.values())
log.info("checkout total %d cents", total)
return total
logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(name)s: %(message)s", stream=sys.stdout)
# A second handler: errors only, into a file, with its own format.
errors = logging.FileHandler("errors.log", encoding="utf-8")
errors.setLevel(logging.ERROR)
errors.setFormatter(logging.Formatter("%(name)s|%(levelname)s|%(message)s"))
logging.getLogger().addHandler(errors)
checkout({"bread": 250, "coffee": 890})
checkout({})
log.error("payment service unreachable")
errors.close()
print("errors.log:")
with open("errors.log", encoding="utf-8") as f:
print(f.read(), end="")
Ausführen mit
python main.pyAusgabe
INFO shop: checkout total 1140 cents
WARNING shop: empty cart, nothing to do
ERROR shop: payment service unreachable
errors.log:
shop|ERROR|payment service unreachableTab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Übung 1 von 3
Das Log-grep liest Zeilen wie 2026-09-29 10:17:12 ERROR db: connection lost. Schreiben Sie parse_line(line), das (Zeit, Level, Meldung) liefert, oder None für eine Zeile in anderer Form, etwa eine Traceback-Zeile. Schreiben Sie level_ok(level, minimum), das True ist, wenn level mindestens so schwer wiegt wie minimum. Der Startcode trennt an Leerzeichen und vergleicht Levelnamen als Text, wobei "ERROR" < "WARNING" gilt.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Ein regulärer Ausdruck mit drei Gruppen passt: Datum und Uhrzeit, ein großgeschriebenes Wort und der Rest. Nutzen Sie fullmatch, nach rstrip("\n").
logging.getLevelNamesMapping() bildet "DEBUG" auf 10 bis "CRITICAL" auf 50 ab. Vergleichen Sie diese Zahlen.
LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)"); geben Sie None zurück, wenn LINE.fullmatch(...) None ist.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import logging
import re
LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")
def parse_line(line: str) -> tuple[str, str, str] | None:
match = LINE.fullmatch(line.rstrip("\n"))
if match is None:
return None
return match.group(1), match.group(2), match.group(3)
def level_ok(level: str, minimum: str) -> bool:
numbers = logging.getLevelNamesMapping()
return numbers.get(level, 0) >= numbers[minimum]
if __name__ == "__main__":
print(parse_line("2026-09-29 10:17:12 ERROR db: connection lost\n"))
print(parse_line("Traceback (most recent call last):\n"))
print(level_ok("ERROR", "WARNING"), level_ok("INFO", "WARNING"))
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
def parse_line(line: str) -> tuple[str, str, str] | None:
date, time, level, message = line.split(" ", 3)
return f"{date} {time}", level, message
def level_ok(level: str, minimum: str) -> bool:
return level >= minimum
if __name__ == "__main__":
print(parse_line("2026-09-29 10:17:12 ERROR db: connection lost\n"))
print(parse_line("Traceback (most recent call last):\n"))
print(level_ok("ERROR", "WARNING"), level_ok("INFO", "WARNING"))
test_main.py
from main import level_ok, parse_line
def test_parse_line():
"""Eine Logzeile zerfällt in Zeit, Level und Meldung"""
got = parse_line("2026-09-29 10:17:12 ERROR db: connection lost\n")
want = ("2026-09-29 10:17:12", "ERROR", "db: connection lost")
assert got == want, f"parse_line lieferte {got!r}"
def test_not_a_log_line():
"""Eine Traceback-Zeile oder eine leere Zeile ergibt None"""
for line in ["Traceback (most recent call last):\n", "\n", " File \"app.py\", line 3\n"]:
got = parse_line(line)
assert got is None, f"für {line!r} lieferte parse_line {got!r}, erwartet war None"
def test_level_ok():
"""ERROR besteht ein Minimum WARNING, INFO nicht"""
assert level_ok("ERROR", "WARNING") is True, "level_ok('ERROR', 'WARNING') sollte True sein"
assert level_ok("INFO", "WARNING") is False, "level_ok('INFO', 'WARNING') sollte False sein"
def test_level_ok_edges():
"""Das Minimum selbst besteht, CRITICAL besteht alles"""
assert level_ok("DEBUG", "DEBUG") is True, "level_ok('DEBUG', 'DEBUG') sollte True sein"
assert level_ok("CRITICAL", "ERROR") is True, "level_ok('CRITICAL', 'ERROR') sollte True sein"
assert level_ok("DEBUG", "INFO") is False, "level_ok('DEBUG', 'INFO') sollte False sein"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 3
Schritt 1 steht fertig im Startcode. Schreiben Sie grep(lines, pattern, minimum="DEBUG", ignore_case=False): Die Funktion liefert die Logzeilen ohne "\n", deren Level minimum besteht und deren Meldung zum regulären Ausdruck pattern passt. ignore_case=True ignoriert Groß- und Kleinschreibung. Zeilen, die keine Logzeilen sind, werden übersprungen und jeweils mit log.debug("line %d skipped: not a log line", number) gemeldet, gezählt ab 1. Führen Sie es aus, um app.log (zweiter Tab) zu durchsuchen.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Kompilieren Sie das Muster einmal mit re.compile(pattern, flags), wobei flags re.IGNORECASE oder 0 ist.
Laufen Sie mit enumerate(lines, start=1), dann haben Sie die Zeilennummer für die debug-Meldung.
Für jede Zeile: parse_line; bei None loggen und continue; sonst die Zeile behalten, wenn level_ok(level, minimum) und regex.search(message) gelten.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import logging
import re
from collections.abc import Iterable
log = logging.getLogger("loggrep")
LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")
def parse_line(line: str) -> tuple[str, str, str] | None:
match = LINE.fullmatch(line.rstrip("\n"))
if match is None:
return None
return match.group(1), match.group(2), match.group(3)
def level_ok(level: str, minimum: str) -> bool:
numbers = logging.getLevelNamesMapping()
return numbers.get(level, 0) >= numbers[minimum]
def grep(lines: Iterable[str], pattern: str, minimum: str = "DEBUG", ignore_case: bool = False) -> list[str]:
regex = re.compile(pattern, re.IGNORECASE if ignore_case else 0)
found = []
for number, line in enumerate(lines, start=1):
parsed = parse_line(line)
if parsed is None:
log.debug("line %d skipped: not a log line", number)
continue
stamp, level, message = parsed
if level_ok(level, minimum) and regex.search(message):
found.append(line.rstrip("\n"))
return found
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
with open("app.log", encoding="utf-8") as f:
for line in grep(f, r"timeout|lost", minimum="INFO", ignore_case=True):
print(line)
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import logging
import re
from collections.abc import Iterable
log = logging.getLogger("loggrep")
LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")
def parse_line(line: str) -> tuple[str, str, str] | None:
match = LINE.fullmatch(line.rstrip("\n"))
if match is None:
return None
return match.group(1), match.group(2), match.group(3)
def level_ok(level: str, minimum: str) -> bool:
numbers = logging.getLevelNamesMapping()
return numbers.get(level, 0) >= numbers[minimum]
def grep(lines: Iterable[str], pattern: str, minimum: str = "DEBUG", ignore_case: bool = False) -> list[str]:
return [line.rstrip("\n") for line in lines if pattern in line]
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
with open("app.log", encoding="utf-8") as f:
for line in grep(f, r"timeout|lost", minimum="INFO", ignore_case=True):
print(line)
test_main.py
import unittest
from main import grep
LOG = [
"2026-09-29 10:15:02 INFO web: server started on port 8000\n",
"2026-09-29 10:16:03 WARNING db: slow query took 2.4 s\n",
"Traceback (most recent call last):\n",
"2026-09-29 10:17:12 ERROR db: connection lost\n",
"2026-09-29 10:18:55 ERROR web: request Timeout for /api/orders\n",
]
def test_regex():
"""Das Muster ist ein regulärer Ausdruck, gesucht in der Meldung"""
got = grep(LOG, r"lost|slow")
want = ["2026-09-29 10:16:03 WARNING db: slow query took 2.4 s", "2026-09-29 10:17:12 ERROR db: connection lost"]
assert got == want, f"grep(LOG, 'lost|slow') lieferte {got!r}"
def test_minimum_level():
"""minimum='ERROR' lässt die WARNING-Zeile weg"""
got = grep(LOG, "db:", minimum="ERROR")
assert got == ["2026-09-29 10:17:12 ERROR db: connection lost"], f"mit minimum='ERROR' lieferte grep {got!r}"
def test_ignore_case():
"""ignore_case=True findet Timeout für das Muster timeout"""
assert grep(LOG, "timeout") == [], "ohne ignore_case sollte timeout nicht zu Timeout passen"
got = grep(LOG, "timeout", ignore_case=True)
assert got == ["2026-09-29 10:18:55 ERROR web: request Timeout for /api/orders"], f"mit ignore_case=True lieferte grep {got!r}"
def test_skipped_line_is_logged():
"""Die Traceback-Zeile wird mit einer DEBUG-Meldung am Logger loggrep übersprungen"""
with unittest.TestCase().assertLogs("loggrep", level="DEBUG") as logs:
grep(LOG, "Traceback")
assert logs.output == ["DEBUG:loggrep:line 3 skipped: not a log line"], f"der Logger loggrep zeichnete {logs.output!r} auf"
app.log
2026-09-29 10:15:02 INFO web: server started on port 8000
2026-09-29 10:15:40 DEBUG db: pool size 5
2026-09-29 10:16:03 WARNING db: slow query took 2.4 s
2026-09-29 10:17:12 ERROR db: connection lost
Traceback (most recent call last):
2026-09-29 10:17:13 INFO db: reconnected after timeout
2026-09-29 10:18:55 ERROR web: request timeout for /api/orders
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 3 von 3
Vollenden Sie loggrep mit main(argv=None) -> int: python main.py MUSTER DATEI [DATEI ...] [--level LEVEL] [-i] [-v]. Geben Sie jeden Treffer als "Datei: Zeile" aus. --level nimmt DEBUG bis CRITICAL; -v stellt Logging mit basicConfig auf DEBUG, sonst auf WARNING. Eine fehlende Datei wird mit log.error("%s: no such file", name) geloggt, die übrigen Dateien werden trotzdem durchsucht. Rückgabe: 0 bei einem Treffer, 1 ohne Treffer, 2 nach einer fehlenden Datei. Führen Sie es auf Ihrem Rechner aus, etwa python main.py timeout app.log -i -v.
Diese Übung braucht Python auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.
Fügen Sie --level mit choices=LEVELS und default="DEBUG" hinzu, dazu -i und -v mit action="store_true".
Rufen Sie logging.basicConfig(level=..., format="%(levelname)s: %(message)s") direkt nach parse_args auf; es schreibt nach stderr und vermischt sich nie mit den Treffern.
Umschließen Sie das open mit try/except FileNotFoundError, merken Sie sich failed = True und machen Sie mit continue weiter. Am Ende: 2 bei failed, sonst 0 bei Treffern, sonst 1.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import argparse
import logging
import re
import sys
from collections.abc import Iterable
log = logging.getLogger("loggrep")
LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")
LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
def parse_line(line: str) -> tuple[str, str, str] | None:
match = LINE.fullmatch(line.rstrip("\n"))
if match is None:
return None
return match.group(1), match.group(2), match.group(3)
def level_ok(level: str, minimum: str) -> bool:
numbers = logging.getLevelNamesMapping()
return numbers.get(level, 0) >= numbers[minimum]
def grep(lines: Iterable[str], pattern: str, minimum: str = "DEBUG", ignore_case: bool = False) -> list[str]:
regex = re.compile(pattern, re.IGNORECASE if ignore_case else 0)
found = []
for number, line in enumerate(lines, start=1):
parsed = parse_line(line)
if parsed is None:
log.debug("line %d skipped: not a log line", number)
continue
stamp, level, message = parsed
if level_ok(level, minimum) and regex.search(message):
found.append(line.rstrip("\n"))
return found
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="loggrep", description="Search log files.")
parser.add_argument("pattern", help="regular expression to search the messages for")
parser.add_argument("files", nargs="+", help="log files to search")
parser.add_argument("--level", default="DEBUG", choices=LEVELS, help="lowest level to show")
parser.add_argument("-i", "--ignore-case", action="store_true")
parser.add_argument("-v", "--verbose", action="store_true", help="log what loggrep does")
args = parser.parse_args(argv)
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING, format="%(levelname)s: %(message)s")
matched = False
failed = False
for name in args.files:
try:
with open(name, encoding="utf-8") as f:
found = grep(f, args.pattern, args.level, args.ignore_case)
except FileNotFoundError:
log.error("%s: no such file", name)
failed = True
continue
for line in found:
print(f"{name}: {line}")
matched = matched or bool(found)
if failed:
return 2
return 0 if matched else 1
if __name__ == "__main__":
sys.exit(main())
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import argparse
import logging
import re
import sys
from collections.abc import Iterable
log = logging.getLogger("loggrep")
LINE = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)")
LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
def parse_line(line: str) -> tuple[str, str, str] | None:
match = LINE.fullmatch(line.rstrip("\n"))
if match is None:
return None
return match.group(1), match.group(2), match.group(3)
def level_ok(level: str, minimum: str) -> bool:
numbers = logging.getLevelNamesMapping()
return numbers.get(level, 0) >= numbers[minimum]
def grep(lines: Iterable[str], pattern: str, minimum: str = "DEBUG", ignore_case: bool = False) -> list[str]:
regex = re.compile(pattern, re.IGNORECASE if ignore_case else 0)
found = []
for number, line in enumerate(lines, start=1):
parsed = parse_line(line)
if parsed is None:
log.debug("line %d skipped: not a log line", number)
continue
stamp, level, message = parsed
if level_ok(level, minimum) and regex.search(message):
found.append(line.rstrip("\n"))
return found
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="loggrep", description="Search log files.")
parser.add_argument("pattern", help="regular expression to search the messages for")
parser.add_argument("files", nargs="+", help="log files to search")
args = parser.parse_args(argv)
for name in args.files:
with open(name, encoding="utf-8") as f:
for line in grep(f, args.pattern):
print(f"{name}: {line}")
return 0
if __name__ == "__main__":
sys.exit(main())
test_main.py
import contextlib
import io
import unittest
from main import main
LOG = (
"2026-09-29 10:16:03 WARNING db: slow query took 2.4 s\n"
"2026-09-29 10:17:12 ERROR db: connection lost\n"
"2026-09-29 10:17:13 INFO db: reconnected after timeout\n"
)
def run(argv):
with open("t_app.log", "w", encoding="utf-8") as f:
f.write(LOG)
out = io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()):
try:
status = main(argv)
except SystemExit as stop:
status = stop.code
return status, out.getvalue().splitlines()
def test_match():
"""Treffer werden mit dem Dateinamen ausgegeben, Status 0"""
status, lines = run(["db", "t_app.log", "--level", "WARNING"])
want = ["t_app.log: 2026-09-29 10:16:03 WARNING db: slow query took 2.4 s", "t_app.log: 2026-09-29 10:17:12 ERROR db: connection lost"]
assert (status, lines) == (0, want), f"main lieferte {status!r} und gab {lines!r} aus"
def test_no_match():
"""Ohne Treffer: Status 1 und keine Ausgabe"""
status, lines = run(["disk", "t_app.log"])
assert (status, lines) == (1, []), f"main lieferte {status!r} und gab {lines!r} aus"
def test_ignore_case():
"""-i findet das kleingeschriebene timeout für TIMEOUT"""
status, lines = run(["-i", "TIMEOUT", "t_app.log"])
assert status == 0 and len(lines) == 1, f"mit -i lieferte main {status!r} und gab {lines!r} aus"
def test_missing_file():
"""Eine fehlende Datei wird als Fehler an loggrep geloggt, Status 2"""
with unittest.TestCase().assertLogs("loggrep", level="ERROR") as logs:
status, lines = run(["db", "missing.log", "t_app.log"])
assert status == 2, f"mit einer fehlenden Datei lieferte main {status!r}, erwartet war 2"
assert logs.output == ["ERROR:loggrep:missing.log: no such file"], f"der Logger loggrep zeichnete {logs.output!r} auf"
assert len(lines) == 3, f"die andere Datei sollte trotzdem durchsucht werden, aber main gab {lines!r} aus"
app.log
2026-09-29 10:15:02 INFO web: server started on port 8000
2026-09-29 10:15:40 DEBUG db: pool size 5
2026-09-29 10:16:03 WARNING db: slow query took 2.4 s
2026-09-29 10:17:12 ERROR db: connection lost
Traceback (most recent call last):
2026-09-29 10:17:13 INFO db: reconnected after timeout
2026-09-29 10:18:55 ERROR web: request timeout for /api/orders
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenimport logging
logging.getLogger("app").setLevel("info")
Was Python ausgibt
ValueError: Unknown level: 'info'Warum, und die Lösung
setLevel und basicConfig(level=...) nehmen einen Levelnamen nur großgeschrieben an, "INFO", denn so sind die Level registriert. Besser ist die Konstante logging.INFO: Ein Tippfehler dort ist sofort ein AttributeError statt eines verwirrenden ValueError.
import logging
import sys
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("{levelname}: {message}"))
Was Python ausgibt
ValueError: Invalid format '{levelname}: {message}' for '%' styleWarum, und die Lösung
Standardmäßig nutzen ein Formatter und basicConfig(format=...) Felder im %-Stil: "%(levelname)s: %(message)s". Geschweifte Klammern funktionieren nur, wenn Sie zusätzlich style="{" übergeben. Die Prüfung läuft beim Erzeugen des Formatters, der Fehler zeigt sich also sofort.
import logging
handler = logging.FileHandler("logs/app.log")
logging.getLogger("app").addHandler(handler)
Was Python ausgibt
No such file or directoryWarum, und die Lösung
FileHandler legt die Datei an, aber nicht den Ordner darum, und löst deshalb FileNotFoundError aus. Legen Sie den Ordner zuerst an: Path("logs").mkdir(exist_ok=True), mit from pathlib import Path.
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.