
When working with HTTP, the response status code is your first and most reliable indicator of what happened on the server side. It’s not just a number; it carries meaning that dictates how your client code should react. Ignoring or misinterpreting these codes leads to fragile applications that break under real-world conditions.
The codes are grouped into five categories, each signaling a different class of response. The 1xx range is informational, mostly irrelevant for typical client-side handling. The 2xx codes mean success, with 200 OK being the most familiar. But don’t get complacent—201 Created or 204 No Content convey different semantics that matter when you process the response body.
3xx codes indicate redirection. They tell the client to fetch another resource or URL. Automatically following redirects is common, but you need awareness when writing APIs or implementing custom HTTP clients, especially with POST requests where the redirect behavior can be tricky.
4xx is the client error range, signaling that the request was somehow malformed or unauthorized. 400 Bad Request means your input was off, while 401 Unauthorized indicates a missing or invalid authentication token. 403 Forbidden means “you’re authenticated but not allowed,” which is a subtle but crucial distinction. Then there’s 404 Not Found, which often means your URL is wrong or the resource was deleted.
Finally, 5xx codes mean server errors. These are less about your client and more about the server being unable to process the request. A 500 Internal Server Error is a catch-all, but 503 Service Unavailable is a hint that the server is temporarily overloaded or down for maintenance—retrying later might make sense.
Here’s a quick snippet to handle these categories properly in Python’s requests library:
import requests
response = requests.get("https://example.com/api/data")
if 200 <= response.status_code < 300:
print("Success:", response.status_code)
elif 300 <= response.status_code < 400:
print("Redirect encountered:", response.status_code)
elif 400 <= response.status_code < 500:
print("Client error:", response.status_code)
elif 500 <= response.status_code < 600:
print("Server error:", response.status_code)
else:
print("Unexpected status code:", response.status_code)
Handling these broadly is better than just checking for 200. In production, you might want to implement more nuanced logic—for example, retrying on 429 Too Many Requests or backing off on 503 with exponential delays. The key is to never treat status codes as mere trivia; they’re your program’s way to communicate with the server’s reality.
Also, remember that not all successful responses contain data. For instance, a 204 No Content means the server intentionally returned no body, so calling response.json() or similar without checking will raise an error. Always check the status code before attempting to parse the content.
Extracting and processing JSON data from responses
Once you’ve verified the status code indicates a response body is present, the next step is to extract that data, often in JSON format. The requests library provides a convenient response.json() method that decodes JSON content into native Python objects. This typically means dictionaries and lists, which you can then manipulate directly.
However, blindly calling response.json() is risky. If the response content isn’t valid JSON, it will raise a json.decoder.JSONDecodeError. To handle this gracefully, wrap the call in a try-except block and respond accordingly. This prevents your application from crashing due to malformed or unexpected server responses.
import requests
response = requests.get("https://example.com/api/data")
if 200 <= response.status_code < 300:
try:
data = response.json()
except ValueError:
print("Response content is not valid JSON")
else:
print("Extracted data:", data)
else:
print(f"Request failed with status code {response.status_code}")
Another important consideration is content-type validation. Even if the status code is 200, the server might respond with HTML, plain text, or some other format. Checking the Content-Type header before parsing JSON can save headaches:
if "application/json" in response.headers.get("Content-Type", ""):
try:
data = response.json()
except ValueError:
print("Invalid JSON received")
else:
print("Response is not JSON, cannot parse")
Once you successfully parse the JSON, you’ll want to verify the structure matches your expectations. JSON APIs often nest information in objects or arrays, so blindly accessing keys can lead to KeyError or TypeError. Defensive programming here means using methods like dict.get() with defaults or validating the presence and types of keys before processing.
For example, if you expect a response like:
{
"status": "ok",
"results": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
}
You might write:
if data.get("status") == "ok" and isinstance(data.get("results"), list):
for item in data["results"]:
print(f"User ID: {item.get('id')}, Name: {item.get('name')}")
else:
print("Unexpected JSON structure")
Remember that APIs evolve. Fields might be added, removed, or renamed, so your client code should be resilient. Avoid hard crashes by using get() and checking types rather than assuming a perfect match. Logging unexpected structures helps you catch API changes early.
In some cases, APIs paginate data, returning only a subset of results with metadata about how to fetch the next page. Look for keys like next or links in the JSON and use them to drive subsequent requests programmatically—don’t hardcode URLs or page numbers.
while True:
response = requests.get(url)
if response.status_code != 200:
break
data = response.json()
process_data(data.get("results", []))
next_url = data.get("next")
if not next_url:
break
url = next_url
By carefully extracting, validating, and processing JSON data, you turn raw HTTP responses into reliable, structured inputs for your application logic. This disciplined approach reduces runtime errors and makes your code more maintainable in the face of changing APIs.

