4. Using raise_for_status()
Manually checking response.ok works, but it adds an if-block every time. There's a cleaner idiom: response.raise_for_status().
It does exactly what the name suggests. For any 4xx or 5xx status code it raises a Python exception; for a 2xx it returns silently and your code moves on. The value isn't just tidier syntax. Without a status check a failed request looks successful to the next line of code, so you can end up three functions deep before something finally crashes trying to parse an HTML error page as JSON. Calling raise_for_status() right after the request fails fast and fails obviously.
The two styles look like this. First the manual version you already know:
def fetch(url):
response = requests.get(url, timeout=5)
if not response.ok:
print(f"Request failed: {response.status_code}")
return None
return response.json()
And the same behaviour written with raise_for_status():
def fetch(url):
try:
response = requests.get(url, timeout=5)
response.raise_for_status() # Raises exception if status >= 400
return response.json()
except requests.exceptions.HTTPError as e:
print(f"Request failed: {e}")
return None
The exception-based version pushes error detection into the handler you were already going to write for network errors. Both styles protect you, but this one keeps the error logic in one place instead of mixing if-checks and except blocks. That's why it's usually considered the more Pythonic of the two.
When to pick each one
| Situation | Use response.ok | Use raise_for_status() |
|---|---|---|
| Simple status check | Best choice | Overkill |
| Already using try/except | Adds extra if-check | Cleaner integration |
| Need to handle specific status codes differently | Check response.status_code directly | Less flexible |
| Production code with error handling | Works fine | More idiomatic |
In short: use raise_for_status() when you're already inside a try/except for network errors, and use response.ok for short checks where you don't want the exception machinery. Both are professional. Pick the one that fits the shape of the code around it.
Refactoring fetch_json_safely
With raise_for_status() in the toolbox, the manual if not response.ok: check from the previous page can move into the try/except already wrapping the request. Same function, same name, same (success, data, error) return contract; the only thing that changes is how the HTTP status check is expressed. Re-save this over your existing fetch_json.py:
import requests
def fetch_json_safely(url, timeout=10):
"""
Fetch JSON from a URL with comprehensive error handling.
Returns: (success: bool, data: dict | None, error: str | None)
"""
try:
# STEP 1: Make the request
response = requests.get(url, timeout=timeout)
# STEP 2: Check the response.
# raise_for_status() routes 4xx/5xx through the HTTPError
# branch below, alongside network errors and timeouts.
response.raise_for_status()
# Verify content type
content_type = response.headers.get("Content-Type", "")
if "application/json" not in content_type:
return False, None, f"Expected JSON, got {content_type}"
# STEP 3: Extract the data
try:
data = response.json()
return True, data, None
except ValueError as e:
return False, None, f"Invalid JSON: {e}"
except requests.exceptions.HTTPError as e:
return False, None, f"HTTP {e.response.status_code}: {e.response.reason}"
except requests.exceptions.Timeout:
return False, None, "Request timed out"
except requests.exceptions.ConnectionError:
return False, None, "Could not connect to server"
except requests.exceptions.RequestException as e:
return False, None, f"Request error: {e}"
# Using the function
success, data, error = fetch_json_safely("https://api.github.com/users/octocat")
if success:
print(f"Name: {data.get('name', 'Unknown')}")
print(f"Location: {data.get('location', 'Unknown')}")
else:
print(f"Failed to fetch data: {error}")
Two changes from the previous version. The manual if not response.ok: branch is gone, replaced by response.raise_for_status(). And there's a new except requests.exceptions.HTTPError clause that catches what raise_for_status() raises and routes it back into the same (success, data, error) tuple shape. The function's external contract is identical; the internals just got tidier. The next section moves on to response headers, which are how you catch the one failure mode a 200 status code doesn't protect you from: getting the wrong kind of content back.