Handling API responses and errors

Handling API responses and errors

APIs are a normal part of modern Python programs, especially when those programs rely on data or services outside our own code. When we make HTTP requests, we are stepping outside our process and depending on a network, a remote server, and an agreed contract. This lesson exists to help us recognize what comes back from an API call and how to react sensibly when things do not go as planned.

Inspecting HTTP response status codes

Every HTTP response includes a status code that tells us, at a high level, how the request went. A successful request typically returns a code in the 200 range, while other ranges indicate redirects, client errors, or server errors.

The requests library exposes this status code directly on the response object, which lets us make decisions before touching the response body.

import requests

response = requests.get("https://example.com/api/planets")

if response.status_code == 200:
    print("Request succeeded")
else:
    print("Request failed")

The status code gives us an immediate signal about whether the response is usable.

Parsing structured response data

Many APIs return structured data rather than plain text, most commonly in JSON format. When an API responds with JSON, we usually want to turn that data into Python dictionaries and lists so we can work with it naturally.

The requests library provides a convenience method for this, assuming the response body contains valid JSON.

import requests

response = requests.get("https://example.com/api/planets")

data = response.json()
print(data["name"])

Once parsed, the response data behaves like any other Python data structure.

Handling unsuccessful responses

Not every request succeeds, even when the server responds cleanly. An API may reject a request due to missing parameters, invalid input, or access restrictions. These cases often return a valid response with a non-success status code.

A common pattern is to check the status code and branch accordingly, handling success and failure as separate paths.

import requests

response = requests.get("https://example.com/api/planets")

if response.status_code != 200:
    print("API returned an error")
    return

planet = response.json()
print(planet["name"])

This keeps error handling explicit and prevents us from assuming a response is valid when it is not.

Detecting and responding to network errors

Sometimes a request never reaches the server or never receives a response. Network outages, DNS failures, and timeouts can all cause this. In these cases, requests raises exceptions instead of returning a response object.

We handle these situations using try and except, which allows us to detect failures outside the normal HTTP response flow.

import requests

try:
    response = requests.get("https://example.com/api/planets", timeout=5)
except requests.RequestException:
    print("Network error occurred")

This distinguishes network-level failures from application-level API responses.

Writing defensive code around API calls

When working with APIs, we assume less and check more. We look at status codes before parsing data, we handle exceptions around network calls, and we avoid treating external responses as guaranteed.

Defensive code makes API usage predictable and keeps failures contained, which is especially important when API calls sit inside larger workflows or automated systems.

Conclusion

We now know how to examine HTTP responses, extract structured data, and react appropriately when requests fail or never complete. With these patterns in place, API calls become safer building blocks rather than fragile points of failure. We are oriented and ready to use external services with confidence.