Abschnitt 17 · Lektion 2
requests
Für eine Liste von Städten müsste man die Adresse jedes Mal zusammenkleben, erst /wetter?stadt=
und dann den Namen. Das geht eine ganze Weile gut. Und dann geht es auf eine Art schief, die man
nicht sofort merkt. requests kann das besser, beim Bauen der Adresse und beim Lesen der Antwort.
Parameter als Dictionary
# 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"
antwort = requests.get(ADRESSE + "/wetter", params={"stadt": "Frankfurt am Main"}, timeout=2)
print(antwort.url)
print(antwort.status_code)
print(antwort) requests.get nimmt die Parameter als Dictionary hinter params=. Der Name des Parameters ist der
Schlüssel, sein Wert ist der Wert. Die Adresse selbst hört dann beim Pfad auf, und requests hängt
das Fragezeichen und alles Weitere an.
Was dabei herauskommt, steht in antwort.url und damit in der ersten Zeile der Ausgabe:
stadt=Frankfurt+am+Main. Jedes Leerzeichen ist zu einem + geworden. In einer Adresse dürfen
nämlich keine Leerzeichen stehen, und ein Umlaut wie in „München“ wird genauso umgeschrieben, zu
M%C3%BCnchen. Beim Dienst wird daraus wieder der Name.
Bei mehreren Parametern musst du dich außerdem nicht um ? und & kümmern. Aus
params={"stadt": "Erfurt", "tage": 3} wird ?stadt=Erfurt&tage=3.
In der letzten Zeile steht, was print(antwort) ausgibt, nämlich <Response [200]>. Das ist die
Antwort als Ganzes, ein Objekt mit Status, Kopfzeilen und Rumpf, und in den eckigen Klammern steht
sein Status. Die Daten selbst stehen da nicht. Die muss man erst herausholen.
# 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"
stadt = "Halle & Umgebung"
von_hand = requests.get(ADRESSE + "/wetter?stadt=" + stadt, timeout=2)
print(von_hand.url)
print(von_hand.text)
mit_params = requests.get(ADRESSE + "/wetter", params={"stadt": stadt}, timeout=2)
print(mit_params.url)
print(mit_params.text) Wenn man die Adresse von Hand zusammenklebt, mit ADRESSE + "/wetter?stadt=" + stadt, klappt das
mit Leerzeichen und Umlauten erstaunlich lange. requests bessert so eine Adresse nämlich noch aus.
Mit urllib aus der letzten Lektion wäre beim Leerzeichen schon Schluss, mit
URL can't contain control characters, und beim Umlaut mit einem UnicodeEncodeError.
Schief geht es bei einem Zeichen, das in einer Adresse eine Aufgabe hat. Ein & trennt zwei
Parameter. Von „Halle & Umgebung“ kommt beim Dienst deshalb nur „Halle “ an, samt Leerzeichen und
ohne jede Warnung. Mit params wird das & als %26 geschrieben und bleibt Teil des Namens.
Beide Male lautet die Antwort „Unbekannte Stadt“. Das liegt nur daran, dass der Dienst keinen Ort mit diesem Namen kennt. Richtig gestellt ist die Frage erst beim zweiten Mal.
Wie lange das Programm wartet
Ohne timeout wartet requests auf eine Antwort, so lange es eben dauert. Wenn der Dienst am anderen
Ende hängt, wartet dein Programm mit, Minuten lang oder für immer, und du siehst nicht, warum es
nicht weitergeht. Mit timeout=2 gibt es nach zwei Sekunden ohne Antwort auf und meldet einen
Fehler.
timeout gehört deshalb an jede Anfrage. Auch wenn der Dienst bisher immer schnell war, denn gerade
dann vergisst man es.
Wie viele Sekunden es sein sollen, hängt vom Dienst ab. Bei einem Dienst im Internet sind fünf oder zehn üblich. Der hier läuft auf derselben Maschine und antwortet in Millisekunden, da sind zwei reichlich. Was passiert, wenn die Zeit abläuft, und wie das Programm dann weitermacht, kommt in der nächsten Lektion.
Erst ansehen, dann hineingreifen
# 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 json
import requests
import wetterdienst
wetterdienst.starten()
ADRESSE = "http://127.0.0.1:8000"
antwort = requests.get(ADRESSE + "/wetter", params={"stadt": "Stuttgart"}, timeout=2)
daten = antwort.json()
print(json.dumps(daten, indent=2, ensure_ascii=False))
print(daten["aktuell"]["wind"], "km/h Wind") antwort.json() gibt ein Dictionary zurück, und das ist oft verschachtelt. In diesem hier stehen
die Stadt, die Uhrzeit der Messung und unter "aktuell" ein zweites Dictionary mit Temperatur,
Wetter und Wind.
Wie eine Antwort aufgebaut ist, steht in der Beschreibung des Dienstes. Schneller ist es, sie sich
einmal anzusehen. Dafür gibt es json.dumps mit indent=2 aus Lektion 12.5, das die Antwort
eingerückt hinschreibt, jede Ebene zwei Leerzeichen weiter rechts. Mit ensure_ascii=False bleibt
das ö ein ö. So siehst du auf einen Blick, wo was liegt.
Danach greift man Ebene für Ebene hinein, mit einer eckigen Klammer je Ebene. daten["aktuell"] ist
das innere Dictionary und daten["aktuell"]["wind"] der Wind darin, 14 km/h in Stuttgart. Wenn du
ein Feld öfter brauchst, bekommt die innere Ebene einen eigenen Namen, aktuell = daten["aktuell"],
und danach reicht aktuell["wind"].
Felder, die man nicht braucht, lässt man einfach liegen, so wie hier die Uhrzeit. Eine Schnittstelle schickt fast immer mehr mit, als ein einzelnes Programm verwendet.
Die Antwort ist noch kein Dictionary
# 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"
antwort = requests.get(ADRESSE + "/wetter", params={"stadt": "Hannover"}, timeout=2)
print(antwort["aktuell"]["temperatur"]) Hier steht antwort["aktuell"], und das endet mit
TypeError: 'Response' object is not subscriptable. Übersetzt heißt das: In ein Objekt vom Typ
Response kann man nicht mit eckigen Klammern hineingreifen. antwort ist die ganze Antwort aus
dem ersten Beispiel. Das Dictionary gibt es erst mit antwort.json().
Fast genauso sieht es aus, wenn die Klammern hinter json fehlen. antwort.json["aktuell"] endet
mit TypeError: 'method' object is not subscriptable, weil json ohne Klammern die Methode selbst
ist und nicht ihr Ergebnis.
Steht in einer Meldung not subscriptable, lohnt sich also immer die Frage, ob vor der eckigen
Klammer wirklich das steht, was du meinst.
Was die Aufgabe will
Diesmal geht es nicht um eine Stadt, sondern um eine ganze Liste, und jede Stadt bekommt ihre eigene Anfrage. Für den Moment antwortet der Dienst bei jeder Stadt auf der Liste freundlich. Was passiert, wenn nicht, ist die nächste Lektion.
Zum Mitnehmen
Mit requests.get(adresse, params={...}, timeout=2) landen die Parameter aus dem Dictionary richtig in der Adresse, auch Leerzeichen, Umlaute und ein &. Fehlt timeout, kann das Programm ewig hängen bleiben. Den Status liest man aus antwort.status_code, und erst antwort.json() liefert das Dictionary, in das man dann Ebene für Ebene hineingreift.
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.