3. The universal request pattern
Every robust API call follows the same three-step shape. Learn it once here and you can apply it to any API in any language for the rest of your career.
The shape is:
- Make the request. Send it with a timeout. This step is where network errors, timeouts, and connection failures live.
- Check the response. Confirm the request succeeded and the content type is what you expected. This stops you processing error pages or the wrong format.
- Extract the data. Parse the body and verify the structure before using any values. This is where you catch
KeyErrors and surprise data types before they crash downstream code.
Every request you make for the rest of your career fits one of these three steps, whether you're fetching JSON, downloading a file, or submitting a form. Let's put it into code.
The pattern in practice
Save this at the project root as fetch_json.py. It's production-ready, and you can adapt it for any JSON API later by changing one line:
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
if not response.ok:
return False, None, f"HTTP {response.status_code}: {response.reason}"
# 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.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}")
A few things are worth noticing about that function. The return contract is a tuple of (success, data, error), so the caller can't accidentally treat a failure as a success, every failure mode produces a specific actionable message rather than a stack trace, and the whole thing is reusable. Point it at any JSON API and it just works.
Testing the pattern against real failures
Claims about resilience are cheap until you actually prove them. The next script deliberately pokes the function at every failure mode we care about, so you can see with your own eyes that each one is caught. Save this as test_pattern.py and run it:
from fetch_json import fetch_json_safely
test_cases = [
("https://httpbin.org/json", "Valid JSON response"),
("https://httpbin.org/status/404", "Returns 404 Not Found"),
("https://httpbin.org/html", "Returns HTML instead of JSON"),
("https://httpbin.org/delay/10", "Takes too long (will timeout)"),
]
for url, description in test_cases:
print(f"\nTest: {description}")
print(f"URL: {url}")
success, data, error = fetch_json_safely(url, timeout=5)
if success:
key_count = len(data)
plural = "" if key_count == 1 else "s"
print(f"✅ Successfully parsed JSON with {key_count} top-level key{plural}")
else:
print(f"❌ {error}")
Run python test_pattern.py and you should see something very close to this:
Test: Valid JSON response
URL: https://httpbin.org/json
✅ Successfully parsed JSON with 1 top-level key
Test: Returns 404 Not Found
URL: https://httpbin.org/status/404
❌ HTTP 404: NOT FOUND
Test: Returns HTML instead of JSON
URL: https://httpbin.org/html
❌ Expected JSON, got text/html; charset=utf-8
Test: Takes too long (will timeout)
URL: https://httpbin.org/delay/10
❌ Request timed out
Every failure mode caught, no crashes, no confusing stack traces. That's defensive programming in practice. The next section takes one specific step of the pattern, the HTTP status check, and shows you a cleaner way to write it.