Zwei Flächen, fünfzehn Minuten
02
Worauf das aufbaut
Voraussetzung: keine
Das ist der Anfang. Dein Programm kann noch nichts.
03
Die Frage
Warum das die erste Lektion ist
Nicht weil Rundung wichtig ist. Weil sie die kleinste Stelle ist, an der eine KI dir selbstsicher falschen Code schreibt — und weil du den Fehler ohne fremde Hilfe sehen kannst, sobald du weisst, wo du hinschauen musst.
Ein float ist keine Dezimalzahl. Er ist ein Bruch mit Zweierpotenzen im Nenner — Basis 2, nicht Basis 10. 0.1 lässt sich darin genauso wenig exakt schreiben wie sich 1/3 im Dezimalsystem exakt schreiben lässt. Du bekommst den nächstgelegenen darstellbaren Wert, und der ist ein winziges Stück daneben.
Das ist kein Bug in Python. Es ist derselbe IEEE-754-Standard in Excel, in JavaScript, in deinem Taschenrechner. Der Unterschied ist nur, wie viele Stellen sie dir zeigen.
Zwei Flächen, und warum es zwei sein müssen
Diese Lektion hat zwei Hälften, und das gilt für jede weitere auch.
Der Browser kann dir in 30 Sekunden beweisen, dass 0.1 + 0.1 + 0.1 nicht 0.3 ist. Er kann dir nicht beweisen, dass dein Projekt eine sinnvolle Struktur hat — denn eine Grenze zwischen zwei Modulen existiert nur zwischen Dateien, und hier gibt es nur eine Zelle.
Das Terminal kann das Gegenteil. Es kann dir zeigen, dass dein Python 3.14 ist, dass es echtes OpenSSL hat, dass ein venv nur ein Ordner ist. Es kann dir keine Antwort in 30 Sekunden geben.
Die Übersetzungsregel
Frag eine KI, wie man Python 3.14 auf einem Mac installiert. Sie wird brew install python@3.14 sagen, oder sudo pip install. Beides ist auf deiner Maschine unmöglich: du bist nicht in der admin-Gruppe, es gibt kein sudo, und Homebrew schreibt nach /opt/homebrew.
Das ist keine Randnotiz, das ist die Fähigkeit, um die es in diesem ganzen Kurs geht. Die Antwort klang richtig. Sie war für diese Maschine falsch. Und du konntest das nur zurückweisen, weil du etwas über die Maschine wusstest, was im Prompt nicht stand.
04
Vorhersage — was passiert hier?
Erst antworten, dann ausführen. Drei Minuten, und es ist das billigste wirksame Ding auf dieser Seite.
0.1 + 0.2round(2.675, 2)round(0.5)round(1.5)
05
Drill
✋ VON HANDKein AI, kein Autocomplete. Tippen.
- Eine Addition, dreimal derselbe Wert. Du brauchst nur
xund+. - Ersetze
...durchx + x + x. Dasreturnsteht schon da. - Dieselbe Form auf anderen Daten:
def doppelt(y): return y + y— das ist alles, was hier fehlt.
Jeder Hinweis wird im Ledger vermerkt. Einen vierten gibt es nicht.
Die nächsten Abschnitte erscheinen, wenn alle Fälle grün sind.
06
Die Entscheidung
Hier ist die Entscheidung, die dieser ganze Kurs bereits getroffen hat. Du sollst sie nicht abnicken, sondern nachvollziehen — und am Ende sagen, wo sie kippen würde.
Die Anforderung: Ein Kurs soll dir Python, Architektur und Finanzmathematik beibringen. Wo laufen die Übungen?
Entwurf A — alles im Browser
Eine Seite mit Code-Zellen. Python läuft als WebAssembly im Tab. Kein Setup, sofortiges Feedback, funktioniert auf dem Telefon in der Bahn.
Entwurf B — alles im Terminal
Statische Lektionstexte, jede Übung in einem echten Repo auf der Platte. Echte Dateien, echtes pytest, echtes git.
Der Tradeoff — fülle das selbst aus, bevor du weiterliest
| Entwurf A (Browser) | Entwurf B (Terminal) | |
|---|---|---|
| Was es kauft | ||
| Was es kostet | ||
| Wann es gewinnt |
Die Frage, um die es geht
Lies die Definition oben nochmal, dann antworte:
> In welchem der beiden Entwürfe kann eine Architektur-Übung überhaupt existieren — und warum > ist das keine Frage von Bequemlichkeit, sondern von Struktur?
Schreib deine Antwort hin, bevor du weiterscrollst. Eine Zeile genügt.
Warum dieser Kurs beides hat
Ein seam existiert nur zwischen Dateien. Eine einzelne Browser-Zelle hat keine Grenzen — also kann sie keine einzige echte Architektur-Übung enthalten. Nicht „schlecht", sondern strukturell unmöglich. Eine Sandbox, die vorgibt ein Projekt zu sein, würde dir ein falsches Modell von der Sache beibringen, die du lernen willst, und du müsstest es später verlernen.
Umgekehrt kann das Terminal dir nicht in 30 Sekunden beweisen, dass round(2.675, 2) gleich 2.67 ist. Es kann es, aber nicht in 30 Sekunden, und nicht ohne dass du erst etwas eingerichtet hast.
Also macht jede Fläche genau das, was nur sie kann:
- Browser: Syntax-Reflexe, Ausgabe vorhersagen, fremden Code beurteilen, Finanz-Intuition.
Und: das Gate. Die späteren Abschnitte dieser Seite existieren nicht im DOM, bis der Drill grün ist.
- Terminal: venv, Import-Pfade,
pytest,git, Module, fremde APIs, die ausfallen, Deploy.
Ungefähr 15 Minuten Browser, 45 Minuten Terminal — aber jede Lektion berührt beide, und die Browser-Hälfte ist nie optional, weil sie das Gate ist.
05b
Lesen & urteilen
Dieser Code ist von einer KI. Nicht jeder davon ist falsch — „sieht korrekt aus, ich akzeptiere“ ist manchmal die richtige Antwort.
Item 1
# Von einer KI geschrieben. Sieht sauber aus: Typ-Hints, Docstring, kurz. # Deine Aufgabe: sagen, ob es korrekt ist — und wenn nicht, mit WELCHER Eingabe # es welchen falschen Wert liefert. Nicht "sieht falsch aus". from decimal import Decimal def als_betrag(wert: float) -> Decimal: """Wandelt einen Geldbetrag in einen exakten Decimal-Wert um. Damit sind Rundungsfehler ausgeschlossen und Beträge können sicher addiert werden. """ return Decimal(wert)Es hat einen Bug.
als_betrag(0.1)liefertDecimal('0.1000000000000000055511151231257827021181583404541015625')und nicht
Decimal('0.1').Der Grund: der Parameter ist ein
float. BisDecimalihn überhaupt zu sehen bekommt, ist der Fehler schon passiert — er steckt im übergebenen Wert.Decimalist ehrlich und zeigt dir den float exakt an, mit allen Stellen, die wirklich drinstehen. Es repariert nichts, es kann nichts reparieren.Der Docstring behauptet damit das Gegenteil von dem, was der Code tut: „Rundungsfehler ausgeschlossen". Der Fehler ist bereits im Argument.
Die Korrektur ist ein Wort — und die Typ-Hint gehört mitgeändert, denn sie ist hier die eigentliche Fehlerquelle:
def als_betrag(wert: str) -> Decimal: return Decimal(wert)Decimal("0.1")ist exakt 1/10.Decimal(0.1)ist exakt der float, der 0.1 am nächsten kommt. Zwei verschiedene Zahlen, ein Zeichen Unterschied.Item 2
# Auch von einer KI. Fast dasselbe wie das Item davor. # Wieder: korrekt oder nicht? Und Vorsicht — die Antwort ist nicht automatisch "Bug". from decimal import Decimal def summiere_positionen(betraege: list[str]) -> Decimal: """Summiert Rechnungspositionen, die als Text vorliegen, exakt. betraege: z.B. ["84.03", "12.50", "7.99"] """ summe = Decimal("0") for betrag in betraege: summe += Decimal(betrag) return summeEs ist korrekt — Akzeptieren war richtig.
Dieser Code ist korrekt. „Sieht gut aus, ich akzeptiere" war die richtige Antwort.
Warum er hält:
- Die Beträge kommen als Text herein, nie als float. Der Fehler aus dem Item davor
kann hier nicht entstehen.
- Der Startwert ist
Decimal("0"), nicht0— der Typ bleibt über die ganze Summe hinweg
derselbe.
Decimal + Decimalist exakt. Es gibt keine Stelle, an der ein float hineinkommt.
Wenn du hier einen Bug gemeldet hast, ist das kein kleiner Fehler, sondern der wichtigste Lerneffekt dieser Lektion:
Eine faire Anmerkung: man könnte über
sum()statt der Schleife oder über fehlende Fehlerbehandlung bei kaputtem Text (Decimal("abc")wirftInvalidOperation) reden. Das sind Stiltragen und fehlende Features — keine falschen Ergebnisse. Ein Review-Urteil unterscheidet zwischen „liefert falsche Zahlen" und „würde ich anders schreiben".
07
Die Spezifikation
Lies das genau. Einen guten Spec zu lesen ist die Vorübung dazu, selbst einen zu schreiben — und das ist die Fähigkeit, die im KI-Zeitalter zählt.
Lies das genau. Einen guten Spec zu lesen ist die Vorübung dazu, selbst einen zu schreiben — und das ist die Fähigkeit, die im KI-Zeitalter tatsächlich zählt. Ab Modul 3 schreibst du sie selbst, und ein Agent baut danach.
Ziel
Nachweisen, dass deine Maschine den Zustand hat, auf dem alle 13 Module aufbauen — und dass die Umstellung deine bestehenden Vertriebsskripte nicht angefasst hat.
Das ist kein Aufwärmen. Wäre dieser Nachweis falsch, würden dir die nächsten Wochen Fehler zeigen, die nach deinen Fehlern aussehen und keine sind.
Zu erfüllende Bedingungen
| Nr | Bedingung | Warum sie drinsteht |
|---|---|---|
| 1 | uv --version läuft in einem neuen Terminal-Tab | Wenn das nur in diesem Tab geht, fehlt die PATH-Zeile in ~/.zshenv und morgen ist alles weg |
| 2 | uv run python -V meldet 3.14.6 | Muss zur Browser-Hälfte passen: Pyodide 314.0.3 ist CPython 3.14.x |
| 3 | ssl.HAS_TLSv1_3 ist True | Auf dem System-Python ist es False (LibreSSL 2.8.3). Ab Modul 8 holst du EZB-Kurse über HTTPS |
| 4 | import tomllib funktioniert | Braucht 3.11+. Der Generator dieser Seite liest damit das Front Matter |
| 5 | /usr/bin/python3 ist weiterhin 3.9.6 | Der eigentliche Test |
| 6 | numpy, pandas, pypdf sind unter 3.9 weiterhin importierbar | Deine Vertriebsskripte hängen daran |
Vertrag
- Eingabe: keine. Der Test prüft die Umgebung, nicht deinen Code.
- Ausgabe:
6 passed. - Fehlerverhalten: jeder Test nennt bei Fehlschlag den tatsächlichen Wert, nicht nur
„assertion failed". Ein Test, der dir nicht sagt, was es stattdessen war, verschwendet deine Zeit.
Ausdrücklich nicht in diesem Modul
- Kein venv, kein
pip install, keinepyproject.toml(Module 1 und 4). - Kein Code unter
src/moneylab/(ab Modul 1). - Kein
uv python install --default. Nie. Siehe die Änderungs-Folgen unten.
Kommandos
Der Acceptance-Test kommt nach tests/test_m00_accept.py. Beachte, wie unhandlich das Kommando ist:
cd ~/Developer/werkbank
uv run --with pytest --python 3.14 pytest tests/test_m00_accept.py -q
08
Vorhersage — das Schloss
Das sind die Namen der Acceptance-Fälle. Die Bodies siehst du noch nicht. Legt fest, welche beim ersten Lauf fehlschlagen werden — und warum.
test_python_ist_3_14test_ssl_kann_tls_1_3test_tomllib_vorhandentest_float_ist_binaertest_decimal_aus_text_ist_exakttest_system_python_unangetastet
Danach erscheint der Kopier-Button. Vorher nicht. Das ist keine Schikane: die festgeschriebene Vorhersage ist das, was bewertet wird — sie zu faken hiesse, sie richtig zu machen.
09
Das Repo-Stück
Datei: tests/test_m00_accept.py
"""Modul 00 — Acceptance-Test.
Prueft die Umgebung, nicht deinen Code. Die sechs Faelle stehen 1:1 in der Spec.
Der wichtigste Test ist der zweitletzte: dass /usr/bin/python3 UNANGETASTET ist. Ein
Upgrade, das die bestehenden Vertriebsskripte still bricht, ist kein Upgrade.
Laufen mit:
uv run --with pytest --python 3.14 pytest tests/test_m00_accept.py -q
"""
from __future__ import annotations
import shutil
import subprocess
import sys
def test_python_ist_3_14():
"""Muss zur Browser-Haelfte passen: Pyodide 314.0.3 ist CPython 3.14.x."""
assert sys.version_info[:2] == (3, 14), (
f"Erwartet 3.14, laeuft aber {sys.version.split()[0]}. "
"Fehlt `uv python pin 3.14`, oder laeuft der Test unter dem falschen Interpreter?"
)
def test_ssl_kann_tls_1_3():
"""Auf dem System-Python ist das False (LibreSSL 2.8.3) — ab Modul 8 zaehlt es."""
import ssl
assert ssl.HAS_TLSv1_3, (
f"Kein TLS 1.3. OPENSSL_VERSION ist {ssl.OPENSSL_VERSION!r}. "
"Erwartet wird ein OpenSSL 3.x aus dem uv-Python, nicht LibreSSL."
)
def test_tomllib_vorhanden():
"""Braucht 3.11+. Der Generator dieser Seite liest damit das Front Matter."""
import tomllib
assert tomllib.loads('a = 1')["a"] == 1
def test_float_ist_binaer():
"""Der Befund dieser Lektion, als Test.
Das ist kein Bug, den wir beheben — es ist eine Eigenschaft, die wir ab Modul 2
umgehen, indem Geld nie ein float ist.
"""
assert 0.1 + 0.2 != 0.3
assert repr(0.1 + 0.2) == "0.30000000000000004"
# Der teure Fall: nicht 2.68.
assert round(2.675, 2) == 2.67
def test_decimal_aus_text_ist_exakt():
"""Und die Loesung, ebenfalls als Test — inklusive der Falle daneben."""
from decimal import Decimal
assert Decimal("0.1") * 3 == Decimal("0.3")
# Aus einem float gebaut hilft Decimal NICHT: der Fehler steckt schon im Argument.
assert Decimal(0.1) != Decimal("0.1")
def test_system_python_unangetastet():
"""DER Test dieses Moduls.
/usr/bin/python3 muss weiterhin 3.9.6 sein und weiterhin numpy/pandas/pypdf
importieren koennen — daran haengen die Pipedrive- und xlsx-Skripte.
Schlaegt das fehl, wurde sehr wahrscheinlich `uv python install --default`
ausgefuehrt. Behebung: `rm ~/.local/bin/python3 ~/.local/bin/python`
"""
system_python = "/usr/bin/python3"
assert shutil.which("python3"), "kein python3 auf dem PATH"
version = subprocess.run(
[system_python, "-c", "import sys; print('%d.%d' % sys.version_info[:2])"],
capture_output=True,
text=True,
timeout=30,
)
assert version.returncode == 0, f"{system_python} laeuft nicht: {version.stderr}"
assert version.stdout.strip() == "3.9", (
f"{system_python} meldet {version.stdout.strip()}, erwartet 3.9. "
"Wurde das System-Python ueberschrieben?"
)
deps = subprocess.run(
[system_python, "-c", "import numpy, pandas, pypdf; print('ok')"],
capture_output=True,
text=True,
timeout=90,
)
assert deps.returncode == 0, (
"Die 3.9-Vertriebs-Dependencies sind nicht mehr importierbar:\n"
f"{deps.stderr.strip()}"
)cd ~/Developer/werkbank
uv run --with pytest --python 3.14 pytest tests/test_m00_accept.py -q
uv run --with ruff ruff check src/
git add -p && git commit10
Die Meisterprüfung
a) Erklären
Ein deutscher Satz, maximal 25 Wörter. Python-Konstrukte zu nennen ist verboten — kein „for-Schleife“, kein „dict“, kein „try/except“. Sag, was der Code tut, nicht wie er aussieht.
b) Änderungs-Folgen
Eine Anforderung ändert sich. Du hast sie noch nicht gesehen. Beantworte sie, bevor du irgendetwas ausprobierst — es geht um die Vorhersage, nicht um das Ergebnis.
Die Änderung
> Du liest in einer Anleitung: „Damit python3 überall die neue Version ist, führe > uv python install --default 3.14 aus." Es klingt aufräumerisch und richtig. Du führst > es aus.
Die Fragen
1. Was genau passiert auf der Platte? Welche Datei oder Dateien entstehen, und wo? 2. Was ist am nächsten Morgen kaputt — und warum merkst du es nicht sofort? 3. Welcher deiner sechs Acceptance-Tests hätte es gefangen? 4. Wie machst du es in einem Befehl rückgängig? 5. Die eigentliche Frage: Warum ist python3.14 harmlos und python3 gefährlich, obwohl beide im selben Ordner liegen und auf dasselbe Binary zeigen?
Und die zweite Hälfte
6. Der Browser fährt Python 3.14.2, dein Terminal 3.14.6. Ist das ein Problem? Begründe die Antwort — und sag, was aus einem harmlosen Unterschied ein schmerzhaftes machen würde. (Ein früherer Entwurf dieses Kurses hatte hier 3.13 gegen 3.14 stehen und ein Build-Gate, das den Unterschied hätte abfangen sollen. Das Gate funktionierte nicht. Warum ist „gleiche Version" ein besserer Schutz als „Prüfung, die Unterschiede findet"?)
11
Abschluss
Drei Felder, dann bist du fertig — und das nächste Modul beginnt mit dem, was du hier schreibst.
Danach im Terminal: uv run python -m werkbank.stand --nach 00, dann committen. Der Commit ist die Aufzeichnung — nicht dieser Browser.