Lesson 5 of 8
Network calls fail in ways local code never does. The server might be down, the connection might
drop, DNS might fail, or the response might simply take forever. Module 6's try/except is
what you need — this lesson is about what to catch.
Always set a timeout
By default, requests waits forever. A hung server hangs your program with it:
import requests
response = requests.get("https://api.example.com/data", timeout=10)
Treat timeout= as mandatory on every request. Ten seconds is a reasonable default.
raise_for_status()
Lesson 8.1 checked .status_code by hand. raise_for_status() turns a 4xx or 5xx into an
exception instead:
import requests
try:
response = requests.get("https://api.example.com/data", timeout=10)
response.raise_for_status()
data = response.json()
except requests.exceptions.HTTPError as e:
print(f"Bad status: {e}")
Both styles are fine. Use if response.ok when a 404 is an expected outcome you want to handle
inline; use raise_for_status() when any bad status is exceptional.
The exceptions to know
All of them inherit from requests.exceptions.RequestException:
| Exception | Cause |
| --- | --- |
| Timeout | took longer than timeout |
| ConnectionError | DNS failure, refused connection, no network |
| HTTPError | raised by raise_for_status() on 4xx/5xx |
| TooManyRedirects | redirect loop |
| RequestException | the base class — catches all of the above |
A robust call handles the specific ones it can act on, then falls back to the base class:
import requests
def fetch(url):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
print("Request timed out")
except requests.exceptions.ConnectionError:
print("Could not connect")
except requests.exceptions.HTTPError as e:
print(f"Server returned an error: {e.response.status_code}")
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
return None
Order matters, exactly as in lesson 6.6: specific first, RequestException last.
Don't forget the JSON
.json() raises if the body isn't valid JSON — common when a server returns an HTML error page
with a 200 status:
import json
def parse_body(text):
try:
return json.loads(text)
except json.JSONDecodeError:
return None
print(parse_body('{"ok": true}'))
print(parse_body("<html>Error page</html>"))
One catch worth knowing: json.JSONDecodeError is a subclass of ValueError. If you catch
ValueError first, it will swallow JSON errors before a later except json.JSONDecodeError
ever runs — the specific-before-general rule from lesson 6.6, with a trap hidden in the class
hierarchy.
Retrying
Transient failures — a timeout, a 503 — are often worth one retry. Two rules:
- Only retry safe operations. A failed
POSTmay have succeeded server-side (lesson 8.4). - Back off exponentially. Waiting 1s, then 2s, then 4s gives a struggling server room to recover. Hammering it immediately makes things worse.
def compute_backoff(attempts):
"""Seconds to wait before each retry: 1, 2, 4, 8..."""
return [2 ** i for i in range(attempts)]
print(compute_backoff(4))
In real code you'd time.sleep() between attempts, and give up after a few.
Never retry a 4xx — your request is wrong, and sending it again won't fix it. A 429 is the
exception: it means slow down, and the response often includes a Retry-After header telling
you how long to wait.
Try it yourself
Handle four different failure modes with one function.
Checkpoint
- Always pass
timeout=— the default is to wait forever. raise_for_status()converts 4xx/5xx into anHTTPError.- Catch
Timeout,ConnectionError,HTTPError, thenRequestExceptionas the catch-all. .json()can fail too — a 200 doesn't guarantee valid JSON.- Retry only safe operations, with exponential backoff; never retry a 4xx except 429.
Questions about this lesson
Log in to ask a question.