Abschnitt 17 · Lektion 3
Wenn das Netz nicht will
Auf eine Antwort kann sich ein Programm im Netz nie ganz verlassen. Auch dann nicht, wenn der Wetterdienst bisher immer geantwortet hat, und zwar mit dem Wetter. Vielleicht kennt der Dienst nicht, wonach du fragst. Vielleicht hat er selbst gerade ein Problem. Oder er antwortet gar nicht, weil er hängt, weil die Verbindung abreißt oder weil das WLAN gerade beschlossen hat, Pause zu machen.
In wetterdienst.py stehen deshalb ganz oben ein paar Städte, bei denen er absichtlich nicht
mitspielt. Unbekannte Städte kennt er natürlich auch nicht.
Eine Stadt, die es nicht gibt
# Der Wetterdienst für diesen Abschnitt. Die Maschine hier hat kein Netz, deshalb
# läuft der Dienst auf derselben Maschine wie dein Programm, unter der Adresse
# http://127.0.0.1:8000. Die Anfragen an ihn sind trotzdem echt: Sie gehen über
# HTTP hinaus und kommen als Antwort zurück, nur eben nicht durch das Internet.
# Hier musst du nichts ändern.
import hashlib
import json
import threading
from datetime import date, timedelta
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
# Das Wetter am 09.10.2026 um 14 Uhr: Grad, Wetter, Wind in km/h.
AKTUELL = {
"Berlin": (14.2, "bewölkt", 12),
"Hamburg": (12.8, "Regen", 24),
"München": (9.6, "Nebel", 3),
"Köln": (15.1, "heiter", 8),
"Frankfurt am Main": (13.4, "bewölkt", 10),
"Stuttgart": (11.9, "Regen", 14),
"Hannover": (13.1, "bewölkt", 16),
"Nürnberg": (10.8, "Nebel", 4),
"Leipzig": (13.7, "heiter", 9),
"Dresden": (12.3, "bewölkt", 11),
"Erfurt": (12.6, "heiter", 7),
}
# Die Vorhersage ab dem 09.10.2026, je Tag: Höchstwert, Tiefstwert, Regen in mm
# und das Wetter.
VORHERSAGE = {
"Leipzig": [
(15.2, 6.1, 0.0, "heiter"),
(17.8, 7.4, 0.0, "sonnig"),
(18.4, 8.0, 0.0, "sonnig"),
(16.9, 9.2, 0.4, "bewölkt"),
(13.5, 8.8, 6.2, "Regen"),
(11.2, 7.5, 9.8, "Regen"),
(12.6, 5.9, 1.1, "bewölkt"),
],
"Dresden": [
(14.6, 5.2, 0.2, "bewölkt"),
(16.3, 6.8, 0.0, "heiter"),
(15.0, 8.1, 0.0, "bewölkt"),
(16.3, 7.7, 0.0, "heiter"),
(19.1, 8.4, 0.0, "sonnig"),
(12.4, 7.0, 5.1, "Regen"),
(10.8, 6.2, 7.6, "Regen"),
],
"Erfurt": [
(13.9, 4.8, 0.0, "heiter"),
(15.6, 5.5, 0.0, "sonnig"),
(14.1, 7.2, 2.3, "Regen"),
(12.0, 6.6, 4.0, "Regen"),
(12.8, 5.1, 0.6, "bewölkt"),
(14.7, 4.3, 0.0, "heiter"),
(13.3, 3.9, 0.0, "heiter"),
],
}
# Für diese Städte klappt etwas nicht, so wie es bei echten Diensten vorkommt.
STOERUNGEN = {
"Bremen": 500, # der Dienst hat selbst einen Fehler
"Kiel": 503, # der Dienst wird gerade gewartet
"Rostock": "keine Antwort", # er nimmt die Anfrage an und antwortet nie
"Lübeck": "Abbruch", # er legt auf, ohne zu antworten
}
# Die Vorhersage gibt es nur mit Schlüssel in der Kopfzeile X-Api-Key. Wie jeder
# ordentliche Dienst kennt dieser den Schlüssel nicht selbst, sondern nur eine
# Prüfsumme davon.
PRUEFSUMME = "0fe6daa0a6754b2239b7022999ec7a5ae2b20fc096afc6d9eead51d07bef68ce"
class Anfrage(BaseHTTPRequestHandler):
def do_GET(self):
adresse = urlparse(self.path)
werte = {name: liste[0] for name, liste in parse_qs(adresse.query).items()}
stadt = werte.get("stadt", "")
stoerung = STOERUNGEN.get(stadt)
if adresse.path not in ("/wetter", "/vorhersage"):
self.sende(404, {"fehler": f"Unbekannte Adresse: {adresse.path}"})
elif stoerung == "keine Antwort":
# Wartet auf etwas, das nie passiert. Der Dienst endet trotzdem mit
# deinem Programm, weil er im Hintergrund läuft.
threading.Event().wait()
elif stoerung == "Abbruch":
self.close_connection = True
elif stoerung is not None:
self.sende(stoerung, self.responses[stoerung][0])
elif adresse.path == "/wetter":
self.wetter(stadt)
else:
self.vorhersage(stadt, werte.get("tage", "7"))
def wetter(self, stadt):
if stadt not in AKTUELL:
self.sende(404, {"fehler": f"Unbekannte Stadt: {stadt}"})
return
grad, wetter, wind = AKTUELL[stadt]
aktuell = {"temperatur": grad, "wetter": wetter, "wind": wind}
self.sende(200, {"stadt": stadt, "zeit": "2026-10-09T14:00", "aktuell": aktuell})
def vorhersage(self, stadt, tage):
schluessel = self.headers.get("X-Api-Key", "")
if hashlib.sha256(schluessel.encode()).hexdigest() != PRUEFSUMME:
self.sende(401, {"fehler": "Ohne gültigen Schlüssel in X-Api-Key gibt es keine Vorhersage"})
elif stadt not in VORHERSAGE:
self.sende(404, {"fehler": f"Keine Vorhersage für: {stadt}"})
elif not tage.isdigit() or not 1 <= int(tage) <= 7:
self.sende(400, {"fehler": "tage geht von 1 bis 7"})
else:
liste = []
for nummer, (hoch, tief, regen, wetter) in enumerate(VORHERSAGE[stadt][: int(tage)]):
liste.append(
{
"datum": (date(2026, 10, 9) + timedelta(days=nummer)).isoformat(),
"wetter": wetter,
"hoechstwert": hoch,
"tiefstwert": tief,
"regen": regen,
}
)
self.sende(200, {"stadt": stadt, "tage": liste})
def sende(self, status, inhalt):
"""Schickt die Antwort: ein Dictionary als JSON, einen Text als Text."""
if isinstance(inhalt, str):
art, rumpf = "text/plain", inhalt
else:
art, rumpf = "application/json", json.dumps(inhalt, ensure_ascii=False)
daten = rumpf.encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", f"{art}; charset=utf-8")
self.send_header("Content-Length", str(len(daten)))
self.end_headers()
self.wfile.write(daten)
def log_message(self, *angaben):
# Sonst stünde jede Anfrage auf dem Fehlerkanal deines Programms.
pass
_server = None
def starten():
"""Startet den Dienst im Hintergrund. Er läuft, bis dein Programm endet."""
global _server
if _server is not None:
return
try:
_server = ThreadingHTTPServer(("127.0.0.1", 8000), Anfrage)
except OSError:
# Port 8000 ist belegt, weil der Dienst aus einem anderen Lauf noch
# antwortet. Dann fragt dein Programm eben den.
return
threading.Thread(target=_server.serve_forever, daemon=True).start() import requests
import wetterdienst
wetterdienst.starten()
ADRESSE = "http://127.0.0.1:8000"
for stadt in ["Nürnberg", "Atlantis"]:
antwort = requests.get(ADRESSE + "/wetter", params={"stadt": stadt}, timeout=2)
print(stadt, antwort.status_code)
print(antwort.json()["aktuell"]["temperatur"]) Nürnberg klappt, Atlantis nicht. Der Traceback endet mit KeyError: 'aktuell', und die Zeile davor
zeigt, was los ist: Atlantis 404.
requests hat also eine Antwort bekommen, nur eben nicht die erhoffte. Für requests ist das kein
Fehler. Eine Antwort ist eine Antwort, auch wenn sie „gibt es nicht“ lautet. Im Rumpf steht dann
kein Wetter, sondern {"fehler": "Unbekannte Stadt: Atlantis"}, und schief geht erst der Griff nach
"aktuell".
Die Meldung zeigt also auf eine Stelle hinter der eigentlichen Ursache. Wer nur sie liest, sucht den Fehler im Dictionary und nicht in der Antwort.
Den Status ansehen
# Der Wetterdienst für diesen Abschnitt. Die Maschine hier hat kein Netz, deshalb
# läuft der Dienst auf derselben Maschine wie dein Programm, unter der Adresse
# http://127.0.0.1:8000. Die Anfragen an ihn sind trotzdem echt: Sie gehen über
# HTTP hinaus und kommen als Antwort zurück, nur eben nicht durch das Internet.
# Hier musst du nichts ändern.
import hashlib
import json
import threading
from datetime import date, timedelta
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
# Das Wetter am 09.10.2026 um 14 Uhr: Grad, Wetter, Wind in km/h.
AKTUELL = {
"Berlin": (14.2, "bewölkt", 12),
"Hamburg": (12.8, "Regen", 24),
"München": (9.6, "Nebel", 3),
"Köln": (15.1, "heiter", 8),
"Frankfurt am Main": (13.4, "bewölkt", 10),
"Stuttgart": (11.9, "Regen", 14),
"Hannover": (13.1, "bewölkt", 16),
"Nürnberg": (10.8, "Nebel", 4),
"Leipzig": (13.7, "heiter", 9),
"Dresden": (12.3, "bewölkt", 11),
"Erfurt": (12.6, "heiter", 7),
}
# Die Vorhersage ab dem 09.10.2026, je Tag: Höchstwert, Tiefstwert, Regen in mm
# und das Wetter.
VORHERSAGE = {
"Leipzig": [
(15.2, 6.1, 0.0, "heiter"),
(17.8, 7.4, 0.0, "sonnig"),
(18.4, 8.0, 0.0, "sonnig"),
(16.9, 9.2, 0.4, "bewölkt"),
(13.5, 8.8, 6.2, "Regen"),
(11.2, 7.5, 9.8, "Regen"),
(12.6, 5.9, 1.1, "bewölkt"),
],
"Dresden": [
(14.6, 5.2, 0.2, "bewölkt"),
(16.3, 6.8, 0.0, "heiter"),
(15.0, 8.1, 0.0, "bewölkt"),
(16.3, 7.7, 0.0, "heiter"),
(19.1, 8.4, 0.0, "sonnig"),
(12.4, 7.0, 5.1, "Regen"),
(10.8, 6.2, 7.6, "Regen"),
],
"Erfurt": [
(13.9, 4.8, 0.0, "heiter"),
(15.6, 5.5, 0.0, "sonnig"),
(14.1, 7.2, 2.3, "Regen"),
(12.0, 6.6, 4.0, "Regen"),
(12.8, 5.1, 0.6, "bewölkt"),
(14.7, 4.3, 0.0, "heiter"),
(13.3, 3.9, 0.0, "heiter"),
],
}
# Für diese Städte klappt etwas nicht, so wie es bei echten Diensten vorkommt.
STOERUNGEN = {
"Bremen": 500, # der Dienst hat selbst einen Fehler
"Kiel": 503, # der Dienst wird gerade gewartet
"Rostock": "keine Antwort", # er nimmt die Anfrage an und antwortet nie
"Lübeck": "Abbruch", # er legt auf, ohne zu antworten
}
# Die Vorhersage gibt es nur mit Schlüssel in der Kopfzeile X-Api-Key. Wie jeder
# ordentliche Dienst kennt dieser den Schlüssel nicht selbst, sondern nur eine
# Prüfsumme davon.
PRUEFSUMME = "0fe6daa0a6754b2239b7022999ec7a5ae2b20fc096afc6d9eead51d07bef68ce"
class Anfrage(BaseHTTPRequestHandler):
def do_GET(self):
adresse = urlparse(self.path)
werte = {name: liste[0] for name, liste in parse_qs(adresse.query).items()}
stadt = werte.get("stadt", "")
stoerung = STOERUNGEN.get(stadt)
if adresse.path not in ("/wetter", "/vorhersage"):
self.sende(404, {"fehler": f"Unbekannte Adresse: {adresse.path}"})
elif stoerung == "keine Antwort":
# Wartet auf etwas, das nie passiert. Der Dienst endet trotzdem mit
# deinem Programm, weil er im Hintergrund läuft.
threading.Event().wait()
elif stoerung == "Abbruch":
self.close_connection = True
elif stoerung is not None:
self.sende(stoerung, self.responses[stoerung][0])
elif adresse.path == "/wetter":
self.wetter(stadt)
else:
self.vorhersage(stadt, werte.get("tage", "7"))
def wetter(self, stadt):
if stadt not in AKTUELL:
self.sende(404, {"fehler": f"Unbekannte Stadt: {stadt}"})
return
grad, wetter, wind = AKTUELL[stadt]
aktuell = {"temperatur": grad, "wetter": wetter, "wind": wind}
self.sende(200, {"stadt": stadt, "zeit": "2026-10-09T14:00", "aktuell": aktuell})
def vorhersage(self, stadt, tage):
schluessel = self.headers.get("X-Api-Key", "")
if hashlib.sha256(schluessel.encode()).hexdigest() != PRUEFSUMME:
self.sende(401, {"fehler": "Ohne gültigen Schlüssel in X-Api-Key gibt es keine Vorhersage"})
elif stadt not in VORHERSAGE:
self.sende(404, {"fehler": f"Keine Vorhersage für: {stadt}"})
elif not tage.isdigit() or not 1 <= int(tage) <= 7:
self.sende(400, {"fehler": "tage geht von 1 bis 7"})
else:
liste = []
for nummer, (hoch, tief, regen, wetter) in enumerate(VORHERSAGE[stadt][: int(tage)]):
liste.append(
{
"datum": (date(2026, 10, 9) + timedelta(days=nummer)).isoformat(),
"wetter": wetter,
"hoechstwert": hoch,
"tiefstwert": tief,
"regen": regen,
}
)
self.sende(200, {"stadt": stadt, "tage": liste})
def sende(self, status, inhalt):
"""Schickt die Antwort: ein Dictionary als JSON, einen Text als Text."""
if isinstance(inhalt, str):
art, rumpf = "text/plain", inhalt
else:
art, rumpf = "application/json", json.dumps(inhalt, ensure_ascii=False)
daten = rumpf.encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", f"{art}; charset=utf-8")
self.send_header("Content-Length", str(len(daten)))
self.end_headers()
self.wfile.write(daten)
def log_message(self, *angaben):
# Sonst stünde jede Anfrage auf dem Fehlerkanal deines Programms.
pass
_server = None
def starten():
"""Startet den Dienst im Hintergrund. Er läuft, bis dein Programm endet."""
global _server
if _server is not None:
return
try:
_server = ThreadingHTTPServer(("127.0.0.1", 8000), Anfrage)
except OSError:
# Port 8000 ist belegt, weil der Dienst aus einem anderen Lauf noch
# antwortet. Dann fragt dein Programm eben den.
return
threading.Thread(target=_server.serve_forever, daemon=True).start() import requests
import wetterdienst
wetterdienst.starten()
ADRESSE = "http://127.0.0.1:8000"
for stadt in ["Nürnberg", "Atlantis", "Bremen"]:
antwort = requests.get(ADRESSE + "/wetter", params={"stadt": stadt}, timeout=2)
if antwort.status_code == 200:
print(stadt, antwort.json()["aktuell"]["temperatur"])
else:
print(stadt, "Status", antwort.status_code, antwort.text) Hier wird der Status angesehen, bevor das Programm in den Rumpf greift. Bei 200 steht das Wetter darin. Bei allem anderen gibt das Programm aus, was zurückkam, und macht mit der nächsten Stadt weiter.
Die Statuscodes sind in Gruppen sortiert, und an der ersten Ziffer sieht man, wessen Problem es ist. Mit 2 beginnt, was geklappt hat.
Mit 4 beginnt ein Problem mit der Anfrage. 404 heißt, das Gefragte gibt es nicht, 401, dass ein Schlüssel fehlt (dazu mehr in der nächsten Lektion), und 429, dass man zu oft gefragt hat. Mit 5 beginnt ein Problem beim Dienst. 500 heißt, dort ist etwas kaputtgegangen, und 503, dass er gerade nicht kann, etwa weil er gewartet wird.
Bei einem 4xx muss sich also an der Anfrage etwas ändern, bei 429 ihr Tempo. Bei einem 5xx hilft oft schon ein späterer Versuch.
Bremen zeigt noch etwas. Der Rumpf ist hier kein JSON, sondern der schlichte Text
Internal Server Error. Das kommt bei einem Fehler im Dienst oft vor, weil dann manchmal nicht
einmal mehr der Teil läuft, der JSON schreibt. Ein antwort.json() darauf endet mit
requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0). Schon beim ersten
Zeichen ist Schluss, weil dort kein JSON anfängt. Auch deshalb kommt der Status zuerst.
Fehlerstatus als Ausnahme
# Der Wetterdienst für diesen Abschnitt. Die Maschine hier hat kein Netz, deshalb
# läuft der Dienst auf derselben Maschine wie dein Programm, unter der Adresse
# http://127.0.0.1:8000. Die Anfragen an ihn sind trotzdem echt: Sie gehen über
# HTTP hinaus und kommen als Antwort zurück, nur eben nicht durch das Internet.
# Hier musst du nichts ändern.
import hashlib
import json
import threading
from datetime import date, timedelta
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
# Das Wetter am 09.10.2026 um 14 Uhr: Grad, Wetter, Wind in km/h.
AKTUELL = {
"Berlin": (14.2, "bewölkt", 12),
"Hamburg": (12.8, "Regen", 24),
"München": (9.6, "Nebel", 3),
"Köln": (15.1, "heiter", 8),
"Frankfurt am Main": (13.4, "bewölkt", 10),
"Stuttgart": (11.9, "Regen", 14),
"Hannover": (13.1, "bewölkt", 16),
"Nürnberg": (10.8, "Nebel", 4),
"Leipzig": (13.7, "heiter", 9),
"Dresden": (12.3, "bewölkt", 11),
"Erfurt": (12.6, "heiter", 7),
}
# Die Vorhersage ab dem 09.10.2026, je Tag: Höchstwert, Tiefstwert, Regen in mm
# und das Wetter.
VORHERSAGE = {
"Leipzig": [
(15.2, 6.1, 0.0, "heiter"),
(17.8, 7.4, 0.0, "sonnig"),
(18.4, 8.0, 0.0, "sonnig"),
(16.9, 9.2, 0.4, "bewölkt"),
(13.5, 8.8, 6.2, "Regen"),
(11.2, 7.5, 9.8, "Regen"),
(12.6, 5.9, 1.1, "bewölkt"),
],
"Dresden": [
(14.6, 5.2, 0.2, "bewölkt"),
(16.3, 6.8, 0.0, "heiter"),
(15.0, 8.1, 0.0, "bewölkt"),
(16.3, 7.7, 0.0, "heiter"),
(19.1, 8.4, 0.0, "sonnig"),
(12.4, 7.0, 5.1, "Regen"),
(10.8, 6.2, 7.6, "Regen"),
],
"Erfurt": [
(13.9, 4.8, 0.0, "heiter"),
(15.6, 5.5, 0.0, "sonnig"),
(14.1, 7.2, 2.3, "Regen"),
(12.0, 6.6, 4.0, "Regen"),
(12.8, 5.1, 0.6, "bewölkt"),
(14.7, 4.3, 0.0, "heiter"),
(13.3, 3.9, 0.0, "heiter"),
],
}
# Für diese Städte klappt etwas nicht, so wie es bei echten Diensten vorkommt.
STOERUNGEN = {
"Bremen": 500, # der Dienst hat selbst einen Fehler
"Kiel": 503, # der Dienst wird gerade gewartet
"Rostock": "keine Antwort", # er nimmt die Anfrage an und antwortet nie
"Lübeck": "Abbruch", # er legt auf, ohne zu antworten
}
# Die Vorhersage gibt es nur mit Schlüssel in der Kopfzeile X-Api-Key. Wie jeder
# ordentliche Dienst kennt dieser den Schlüssel nicht selbst, sondern nur eine
# Prüfsumme davon.
PRUEFSUMME = "0fe6daa0a6754b2239b7022999ec7a5ae2b20fc096afc6d9eead51d07bef68ce"
class Anfrage(BaseHTTPRequestHandler):
def do_GET(self):
adresse = urlparse(self.path)
werte = {name: liste[0] for name, liste in parse_qs(adresse.query).items()}
stadt = werte.get("stadt", "")
stoerung = STOERUNGEN.get(stadt)
if adresse.path not in ("/wetter", "/vorhersage"):
self.sende(404, {"fehler": f"Unbekannte Adresse: {adresse.path}"})
elif stoerung == "keine Antwort":
# Wartet auf etwas, das nie passiert. Der Dienst endet trotzdem mit
# deinem Programm, weil er im Hintergrund läuft.
threading.Event().wait()
elif stoerung == "Abbruch":
self.close_connection = True
elif stoerung is not None:
self.sende(stoerung, self.responses[stoerung][0])
elif adresse.path == "/wetter":
self.wetter(stadt)
else:
self.vorhersage(stadt, werte.get("tage", "7"))
def wetter(self, stadt):
if stadt not in AKTUELL:
self.sende(404, {"fehler": f"Unbekannte Stadt: {stadt}"})
return
grad, wetter, wind = AKTUELL[stadt]
aktuell = {"temperatur": grad, "wetter": wetter, "wind": wind}
self.sende(200, {"stadt": stadt, "zeit": "2026-10-09T14:00", "aktuell": aktuell})
def vorhersage(self, stadt, tage):
schluessel = self.headers.get("X-Api-Key", "")
if hashlib.sha256(schluessel.encode()).hexdigest() != PRUEFSUMME:
self.sende(401, {"fehler": "Ohne gültigen Schlüssel in X-Api-Key gibt es keine Vorhersage"})
elif stadt not in VORHERSAGE:
self.sende(404, {"fehler": f"Keine Vorhersage für: {stadt}"})
elif not tage.isdigit() or not 1 <= int(tage) <= 7:
self.sende(400, {"fehler": "tage geht von 1 bis 7"})
else:
liste = []
for nummer, (hoch, tief, regen, wetter) in enumerate(VORHERSAGE[stadt][: int(tage)]):
liste.append(
{
"datum": (date(2026, 10, 9) + timedelta(days=nummer)).isoformat(),
"wetter": wetter,
"hoechstwert": hoch,
"tiefstwert": tief,
"regen": regen,
}
)
self.sende(200, {"stadt": stadt, "tage": liste})
def sende(self, status, inhalt):
"""Schickt die Antwort: ein Dictionary als JSON, einen Text als Text."""
if isinstance(inhalt, str):
art, rumpf = "text/plain", inhalt
else:
art, rumpf = "application/json", json.dumps(inhalt, ensure_ascii=False)
daten = rumpf.encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", f"{art}; charset=utf-8")
self.send_header("Content-Length", str(len(daten)))
self.end_headers()
self.wfile.write(daten)
def log_message(self, *angaben):
# Sonst stünde jede Anfrage auf dem Fehlerkanal deines Programms.
pass
_server = None
def starten():
"""Startet den Dienst im Hintergrund. Er läuft, bis dein Programm endet."""
global _server
if _server is not None:
return
try:
_server = ThreadingHTTPServer(("127.0.0.1", 8000), Anfrage)
except OSError:
# Port 8000 ist belegt, weil der Dienst aus einem anderen Lauf noch
# antwortet. Dann fragt dein Programm eben den.
return
threading.Thread(target=_server.serve_forever, daemon=True).start() import requests
import wetterdienst
wetterdienst.starten()
ADRESSE = "http://127.0.0.1:8000"
for stadt in ["Nürnberg", "Atlantis", "Bremen"]:
antwort = requests.get(ADRESSE + "/wetter", params={"stadt": stadt}, timeout=2)
try:
antwort.raise_for_status()
except requests.exceptions.HTTPError as fehler:
print(fehler)
continue
print(stadt, antwort.json()["aktuell"]["temperatur"]) Mit antwort.raise_for_status() lässt sich dasselbe auf eine zweite Art prüfen. Bei einem Status
mit 2 vorn passiert nichts. Bei einem mit 4 oder 5 vorn kommt ein requests.exceptions.HTTPError,
und an seiner Meldung siehst du gleich, was los ist: 404 Client Error: Not Found for url: … und
500 Server Error: Internal Server Error for url: …. Client Error heißt, das Problem liegt bei der
Anfrage, Server Error heißt, beim Dienst. Das sind die beiden Gruppen von oben.
Welche der beiden Arten man nimmt, hängt davon ab, was danach kommen soll. Wenn das Programm je nach
Status etwas anderes tun will, wie in der Schleife oben, ist status_code meistens übersichtlicher.
Kann es ohne die Daten sowieso nicht weitermachen, reicht ein einzelnes antwort.raise_for_status()
direkt nach der Anfrage, ganz ohne try. Dann bricht das Programm an der richtigen Stelle ab, mit
einer Meldung, in der der Status steht, und nicht erst drei Zeilen später mit einem KeyError.
Bei urllib ist das übrigens anders. Dort löst urlopen bei 404 von sich aus eine Ausnahme aus,
bei requests passiert das nur, wenn man danach fragt.
Wenn keine Antwort kommt
# Der Wetterdienst für diesen Abschnitt. Die Maschine hier hat kein Netz, deshalb
# läuft der Dienst auf derselben Maschine wie dein Programm, unter der Adresse
# http://127.0.0.1:8000. Die Anfragen an ihn sind trotzdem echt: Sie gehen über
# HTTP hinaus und kommen als Antwort zurück, nur eben nicht durch das Internet.
# Hier musst du nichts ändern.
import hashlib
import json
import threading
from datetime import date, timedelta
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
# Das Wetter am 09.10.2026 um 14 Uhr: Grad, Wetter, Wind in km/h.
AKTUELL = {
"Berlin": (14.2, "bewölkt", 12),
"Hamburg": (12.8, "Regen", 24),
"München": (9.6, "Nebel", 3),
"Köln": (15.1, "heiter", 8),
"Frankfurt am Main": (13.4, "bewölkt", 10),
"Stuttgart": (11.9, "Regen", 14),
"Hannover": (13.1, "bewölkt", 16),
"Nürnberg": (10.8, "Nebel", 4),
"Leipzig": (13.7, "heiter", 9),
"Dresden": (12.3, "bewölkt", 11),
"Erfurt": (12.6, "heiter", 7),
}
# Die Vorhersage ab dem 09.10.2026, je Tag: Höchstwert, Tiefstwert, Regen in mm
# und das Wetter.
VORHERSAGE = {
"Leipzig": [
(15.2, 6.1, 0.0, "heiter"),
(17.8, 7.4, 0.0, "sonnig"),
(18.4, 8.0, 0.0, "sonnig"),
(16.9, 9.2, 0.4, "bewölkt"),
(13.5, 8.8, 6.2, "Regen"),
(11.2, 7.5, 9.8, "Regen"),
(12.6, 5.9, 1.1, "bewölkt"),
],
"Dresden": [
(14.6, 5.2, 0.2, "bewölkt"),
(16.3, 6.8, 0.0, "heiter"),
(15.0, 8.1, 0.0, "bewölkt"),
(16.3, 7.7, 0.0, "heiter"),
(19.1, 8.4, 0.0, "sonnig"),
(12.4, 7.0, 5.1, "Regen"),
(10.8, 6.2, 7.6, "Regen"),
],
"Erfurt": [
(13.9, 4.8, 0.0, "heiter"),
(15.6, 5.5, 0.0, "sonnig"),
(14.1, 7.2, 2.3, "Regen"),
(12.0, 6.6, 4.0, "Regen"),
(12.8, 5.1, 0.6, "bewölkt"),
(14.7, 4.3, 0.0, "heiter"),
(13.3, 3.9, 0.0, "heiter"),
],
}
# Für diese Städte klappt etwas nicht, so wie es bei echten Diensten vorkommt.
STOERUNGEN = {
"Bremen": 500, # der Dienst hat selbst einen Fehler
"Kiel": 503, # der Dienst wird gerade gewartet
"Rostock": "keine Antwort", # er nimmt die Anfrage an und antwortet nie
"Lübeck": "Abbruch", # er legt auf, ohne zu antworten
}
# Die Vorhersage gibt es nur mit Schlüssel in der Kopfzeile X-Api-Key. Wie jeder
# ordentliche Dienst kennt dieser den Schlüssel nicht selbst, sondern nur eine
# Prüfsumme davon.
PRUEFSUMME = "0fe6daa0a6754b2239b7022999ec7a5ae2b20fc096afc6d9eead51d07bef68ce"
class Anfrage(BaseHTTPRequestHandler):
def do_GET(self):
adresse = urlparse(self.path)
werte = {name: liste[0] for name, liste in parse_qs(adresse.query).items()}
stadt = werte.get("stadt", "")
stoerung = STOERUNGEN.get(stadt)
if adresse.path not in ("/wetter", "/vorhersage"):
self.sende(404, {"fehler": f"Unbekannte Adresse: {adresse.path}"})
elif stoerung == "keine Antwort":
# Wartet auf etwas, das nie passiert. Der Dienst endet trotzdem mit
# deinem Programm, weil er im Hintergrund läuft.
threading.Event().wait()
elif stoerung == "Abbruch":
self.close_connection = True
elif stoerung is not None:
self.sende(stoerung, self.responses[stoerung][0])
elif adresse.path == "/wetter":
self.wetter(stadt)
else:
self.vorhersage(stadt, werte.get("tage", "7"))
def wetter(self, stadt):
if stadt not in AKTUELL:
self.sende(404, {"fehler": f"Unbekannte Stadt: {stadt}"})
return
grad, wetter, wind = AKTUELL[stadt]
aktuell = {"temperatur": grad, "wetter": wetter, "wind": wind}
self.sende(200, {"stadt": stadt, "zeit": "2026-10-09T14:00", "aktuell": aktuell})
def vorhersage(self, stadt, tage):
schluessel = self.headers.get("X-Api-Key", "")
if hashlib.sha256(schluessel.encode()).hexdigest() != PRUEFSUMME:
self.sende(401, {"fehler": "Ohne gültigen Schlüssel in X-Api-Key gibt es keine Vorhersage"})
elif stadt not in VORHERSAGE:
self.sende(404, {"fehler": f"Keine Vorhersage für: {stadt}"})
elif not tage.isdigit() or not 1 <= int(tage) <= 7:
self.sende(400, {"fehler": "tage geht von 1 bis 7"})
else:
liste = []
for nummer, (hoch, tief, regen, wetter) in enumerate(VORHERSAGE[stadt][: int(tage)]):
liste.append(
{
"datum": (date(2026, 10, 9) + timedelta(days=nummer)).isoformat(),
"wetter": wetter,
"hoechstwert": hoch,
"tiefstwert": tief,
"regen": regen,
}
)
self.sende(200, {"stadt": stadt, "tage": liste})
def sende(self, status, inhalt):
"""Schickt die Antwort: ein Dictionary als JSON, einen Text als Text."""
if isinstance(inhalt, str):
art, rumpf = "text/plain", inhalt
else:
art, rumpf = "application/json", json.dumps(inhalt, ensure_ascii=False)
daten = rumpf.encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", f"{art}; charset=utf-8")
self.send_header("Content-Length", str(len(daten)))
self.end_headers()
self.wfile.write(daten)
def log_message(self, *angaben):
# Sonst stünde jede Anfrage auf dem Fehlerkanal deines Programms.
pass
_server = None
def starten():
"""Startet den Dienst im Hintergrund. Er läuft, bis dein Programm endet."""
global _server
if _server is not None:
return
try:
_server = ThreadingHTTPServer(("127.0.0.1", 8000), Anfrage)
except OSError:
# Port 8000 ist belegt, weil der Dienst aus einem anderen Lauf noch
# antwortet. Dann fragt dein Programm eben den.
return
threading.Thread(target=_server.serve_forever, daemon=True).start() import time
import requests
import wetterdienst
wetterdienst.starten()
ADRESSE = "http://127.0.0.1:8000"
start = time.perf_counter()
for stadt in ["Rostock", "Lübeck", "Nürnberg"]:
try:
antwort = requests.get(ADRESSE + "/wetter", params={"stadt": stadt}, timeout=1)
except requests.exceptions.RequestException as fehler:
print(stadt, "keine Antwort:", fehler)
continue
print(stadt, antwort.status_code)
print(f"Fertig nach {time.perf_counter() - start:.1f} Sekunden") Ohne Antwort gibt es auch keinen Status, den man ansehen könnte. Dann löst schon requests.get eine
Ausnahme aus, und an ihrer Fehlerart erkennt man, was passiert ist.
Rostock nimmt die Anfrage an und antwortet nie. Nach einer Sekunde, so lange wie timeout=1
erlaubt, gibt requests auf. Das ist ein ReadTimeout, eine Art von requests.exceptions.Timeout,
und die Meldung endet auf Read timed out. (read timeout=1).
Bei Lübeck legt der Dienst auf, ohne zu antworten. Das ist ein
requests.exceptions.ConnectionError, wie in dem Beispiel, in dem niemand zu Hause war. Ohne
timeout stünde das Programm bei Rostock still, bis es die Maschine am Zeitlimit beendet. Zu Hause
stünde es einfach still.
Beide Arten gehören zur Familie requests.exceptions.RequestException, zu der alle Fehler von
requests gehören, und die fängt das except im Beispiel. Ein except Exception ginge hier auch.
Nur fängt es wie in Lektion 9.2 jeden Tippfehler in derselben Zeile gleich mit. In der Rechnung am
Ende siehst du eine Sekunde für Rostock und fast nichts für den Rest.
Eine Falle hat die Familie, denn auch der JSONDecodeError von oben gehört dazu. Wenn
antwort.json() mit im try steht, landet ein Rumpf, der kein JSON ist, im selben except. Dann
heißt es „keine Antwort“, obwohl eine kam. Das try gehört deshalb wie schon in Lektion 9.2 nur um
die Zeile, die wirklich scheitern kann. Hier ist das requests.get.
Was die Aufgabe will
In der Aufgabe steht das Programm aus der letzten Lektion und eine Liste, in der so ziemlich alles vorkommt, was oben schiefging. Am Ende soll jede Stadt ihre Zeile bekommen und das Programm bis zum Schluss durchlaufen.
Zum Mitnehmen
Auch eine Antwort mit Status 404 oder 500 ist für requests eine Antwort. Ob die Daten wirklich da sind, sieht man erst an status_code oder raise_for_status(). Bei 4xx stimmt etwas mit der Anfrage nicht, bei 5xx mit dem Dienst. Bleibt die Antwort ganz aus, gibt es einen Timeout oder einen ConnectionError, beide fängt requests.exceptions.RequestException, und das try steht nur um die Anfrage.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.