Chapter 4: Safe and Reliable API Requests
1. When simple isn't enough
In this chapter you'll turn Chapter 3's four-line API call into code that doesn't fall over the moment something goes wrong: code that checks every response before trusting it, reads headers to know what came back, and handles rate limits, network errors, and binary data instead of crashing on them. By the end you'll have a Make → Check → Extract pattern you can apply to every API request you write from here on.
Chapter 3 showed you that talking to an API can be as short as four lines of Python. That code works. It also only works most of the time, and this chapter is about the difference.
Here's the snippet we'll keep coming back to:
import requests
response = requests.get("https://catfact.ninja/fact")
data = response.json()
print(data["fact"])
It's clean. It's simple. And on a fast connection at your desk, the call is reliable. But it carries four silent vulnerabilities that only show up in production:
- No timeout. If catfact.ninja is slow, your program hangs forever waiting for a response that may never come.
- No status check. If the server returns a 503 error page, the next line tries to parse HTML as JSON and crashes.
- No content check. If the response isn't JSON at all,
response.json()throws aJSONDecodeError. - No key check. If the API changes and renames the field,
data["fact"]throws aKeyError.
The APIs you used in Chapter 3 were intentionally friendly: stable servers, predictable JSON, designed for learners. Real-world APIs are less polite. Servers return errors, networks drop requests, responses arrive in shapes you didn't expect, and a request that worked yesterday can behave differently today. The more your code assumes everything will go right, the more fragile it becomes.
The shift we're making in this chapter is less technical than psychological. Amateur code assumes success and crashes when that assumption breaks. Professional code assumes things will occasionally fail, plans for it, and keeps running. That's what users actually rely on.
Over the next seven sections you'll turn the fragile snippet above into code that handles real-world conditions gracefully: it'll catch network failures, recognise bad responses before parsing them, distinguish different kinds of errors, and still deliver something useful to the caller when something goes wrong.
- Validate responses before processing data, so failures surface early instead of silently
- Read response headers to detect content types and handle different formats appropriately
- Distinguish network errors, HTTP errors, and data-quality issues, and respond to each
- Apply the Make → Check → Extract pattern consistently to every API request
- Work confidently with binary responses like images, not just JSON text
- Build applications that degrade gracefully instead of crashing when an API misbehaves
fetch_json.py— a defensive JSON fetcher applying Make → Check → Extract; refactored mid-chapter to useraise_for_status()test_pattern.py— deliberate failure tests that exercise each error path offetch_json_safelyinspect_headers.py— reader that pulls Content-Type, Content-Length, and rate-limit headerscheck_rate_limits.py— rate-limit header parsing with backoff awarenesstext_vs_binary.py— comparison of text and binary response accessorssave_image_wrong.py— deliberate text-mode mistake for binary datasave_image.py— corrected binary-write version of the image savedownloader.py— binary image downloader with content-type validation and safe write-mode handlinguse_downloader.py— small caller that unpacks the downloader's return tuple
First we'll see what happens when you skip these checks entirely, then build the pattern that prevents it. Onwards.