mitmario.dev

Wenn das Netz nicht will

Python Sandbox 4 Min Lesezeit 4 BeispieleLektion 3 von 5

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

Eine Stadt, die es nicht gibt
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

Den Status ansehen
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

Fehlerstatus als Ausnahme
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

Wenn keine Antwort kommt
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, kostenlos

Zu 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.