Chapter 21 Quiz: Working with APIs and External Data Services

Answer all questions before checking the answer key at the end. Questions vary in format: multiple choice, true/false, short answer, and code analysis.


Section A: Multiple Choice (1 point each)

Question 1. What does API stand for?

a) Automated Protocol Interface b) Application Programming Interface c) Advanced Python Integration d) Application Process Input


Question 2. When you call response.json() on a requests response object, what does it return?

a) A JSON string that you must manually parse b) The raw HTTP response body as bytes c) A Python dictionary or list, depending on the response structure d) A file-like object you must iterate over


Question 3. Which HTTP method is used to retrieve data from an API without modifying anything?

a) POST b) PUT c) GET d) FETCH


Question 4. You receive a 401 status code from an API. What does this almost certainly mean?

a) The server is down and you should retry later b) Your request was successful c) The resource you requested does not exist d) Your API key is invalid or missing


Question 5. Which of the following is the correct way to store and access an API key in Python?

a) api_key = "sk_live_abc123" at the top of your script b) api_key = os.environ.get("MY_API_KEY") after loading from the environment c) api_key = input("Enter your API key: ") each time the script runs d) api_key = open("api_key.txt").read() from a plaintext file in the project directory


Question 6. In the requests library, how do you pass query parameters to a GET request?

a) requests.get(url + "?param=value") b) requests.get(url, data={"param": "value"}) c) requests.get(url, params={"param": "value"}) d) requests.get(url, query={"param": "value"})


Question 7. What does a 429 status code indicate?

a) The server encountered an internal error b) Your request was malformed c) You have been rate limited and sent too many requests d) The endpoint you requested does not exist


Question 8. What is exponential backoff?

a) Increasing the size of each API response to reduce the number of requests needed b) Waiting progressively longer between retry attempts (e.g., 1s, 2s, 4s, 8s...) c) Reducing the number of query parameters with each retry d) Splitting large API responses into exponentially smaller chunks


Question 9. What is the purpose of pagination in an API?

a) To encrypt the response data for security b) To split large result sets into multiple smaller requests rather than returning everything at once c) To compress the response body for faster transmission d) To allow multiple users to share a single API key


Question 10. Which code snippet correctly reads a Bearer token from an environment variable and uses it in an API call?

Option A:

headers = {"Authorization": "Bearer " + "MY_TOKEN"}

Option B:

token = os.environ.get("API_TOKEN")
headers = {"Authorization": f"Bearer {token}"}

Option C:

headers = {"Bearer": os.environ.get("API_TOKEN")}

Option D:

headers = {"Token": "Bearer", "Value": os.environ.get("API_TOKEN")}

a) Option A b) Option B c) Option C d) Option D


Section B: True or False (1 point each)

Question 11. A REST API response always includes a status code, regardless of whether the request succeeded.

Question 12. Calling response.raise_for_status() on a 200 response will raise an exception.

Question 13. Query parameters in a GET request are visible in the URL and therefore should never be used to transmit API keys.

Question 14. The requests library comes pre-installed with Python and does not need to be installed separately.

Question 15. When an API returns a 503 status code (Service Unavailable), retrying the same request later is a reasonable response.


Section C: Code Analysis (2 points each)

Question 16. What is wrong with this code?

import requests

response = requests.get("https://open.er-api.com/v6/latest/USD")
data = response.json()
eur_rate = data["rates"]["EUR"]
print(f"EUR rate: {eur_rate}")

Identify at least two problems and explain how to fix them.


Question 17. Analyze this pagination loop. Is it correct? If not, what could go wrong?

all_results = []
page = 1

while True:
    response = requests.get(url, params={"page": page, "pageSize": 50})
    data = response.json()
    results = data["items"]
    all_results.extend(results)
    page += 1

Question 18. This retry function has a bug. Find it:

def fetch_with_retry(url, max_retries=3):
    for attempt in range(max_retries):
        response = requests.get(url, timeout=10)
        if response.status_code == 200:
            return response.json()
        if response.status_code == 429:
            time.sleep(2 ** attempt)
    raise RuntimeError("All retries failed")

Question 19. What does this code do, and is the approach correct?

def safe_get_revenue(data):
    try:
        return data["company"]["financials"]["annual"]["revenue"]
    except KeyError:
        return None
    except TypeError:
        return None

Section D: Short Answer (3 points each)

Question 20. Priya needs to build a script that makes 500 API calls to fetch data on Acme's 500 customer accounts. The API has a rate limit of 60 requests per minute. Describe how you would structure the loop to stay within the rate limit, including: the calculation for how long to wait between requests, and how to handle a 429 response if you accidentally exceed it.


Question 21. Explain the difference between storing an API key in a .env file versus storing it in a system environment variable. When would you prefer each approach? What must you remember to do with the .env file to ensure the key stays secure?


Question 22. The Open-Meteo weather API returns this JSON structure:

{
  "current_weather": {
    "temperature": 12.4,
    "windspeed": 18.2,
    "weathercode": 3,
    "time": "2024-10-28T14:00"
  },
  "hourly": {
    "time": ["2024-10-28T00:00", "2024-10-28T01:00"],
    "temperature_2m": [10.1, 9.8]
  }
}

Write a Python function extract_weather_summary(response_data) that returns a plain English string describing the current conditions — for example: "Currently 12.4°C (54.3°F) with wind at 18.2 km/h (11.3 mph)". Include the Fahrenheit and mph conversions in your implementation.


Question 23. A colleague suggests handling all API errors like this:

try:
    response = requests.get(url, timeout=30)
    data = response.json()
    return data
except Exception as e:
    print(f"Something went wrong: {e}")
    return None

Explain why this is a poor approach. What are the specific problems? Write a better version.


Answer Key

Section A: 1. b 2. c 3. c 4. d 5. b 6. c 7. c 8. b 9. b 10. b

Section B: 11. True — every HTTP response includes a status code; it is part of the HTTP protocol 12. False — raise_for_status() only raises an exception for 4xx and 5xx status codes; 200 is a success code and raises nothing 13. True — query parameters appear in the URL, in browser history, in server logs, and in any proxy logs between the client and server; headers are a better choice 14. False — requests is a third-party library and must be installed with pip install requests 15. True — 503 Service Unavailable usually indicates a temporary server problem; retrying later (with appropriate backoff) is the correct response

Section C:

  1. Problems: (1) No timeout parameter — the request could hang indefinitely if the server is slow or unresponsive (2) No status code check before calling response.json() — if the response is a 429 or 500, response.json() may fail or return an error structure that doesn't contain "rates" (3) data["rates"]["EUR"] uses direct access rather than .get() — will crash with KeyError if the structure is unexpected Fixed version: Add timeout=15, check response.raise_for_status(), use data.get("rates", {}).get("EUR")

  2. Bug: The loop has no termination condition other than receiving an empty results list — but the code never checks for that. If the API always returns at least one item, this loop runs forever. Additionally, there is no max_pages safety limit, no delay between requests (likely to trigger rate limiting), and no status code check. Fix: Add if not results: break, add a max_pages guard, add time.sleep() between requests, and check response.raise_for_status().

  3. Bug: range(max_retries) iterates max_retries times (0, 1, 2 when max_retries=3) — but the function description says it should try max_retries times before failing. Combined with a non-200, non-429 status code (like 500), the function never retries those errors — it just increments the loop and on the third iteration, exits the loop and raises RuntimeError even though retrying might have worked. Fix: Use range(max_retries + 1) and explicitly handle 500-level errors with retry logic.

  4. What it does: It attempts to navigate a deeply nested dictionary and returns None if any key is missing (KeyError) or if a value along the path is not a dictionary (TypeError, which would be raised when you try to subscript None["key"]). Is it correct? Yes, the approach is valid. Catching both KeyError and TypeError handles the two most common failure modes for nested dict access. A logging call before return None would make debugging easier in production.

Section D:

  1. Rate limit compliance: At 60 requests/minute, you can make 1 request per second. Between each request, add time.sleep(1.0) (or slightly more, like time.sleep(1.1)) to give a small buffer. For 500 requests, total runtime is approximately 8-9 minutes. For 429 handling: catch the 429 response, read the Retry-After header if present, sleep for that duration (or 60 seconds as a fallback), then continue from where you left off. Use a loop variable to track progress so you can resume without repeating requests if the script is interrupted.

  2. .env file: Stores credentials in a file in the project directory. Requires python-dotenv to load. Easy to set up per-project. Can be version controlled by accident if you forget .gitignore. Best for development and personal projects. System environment variable: Set at the OS level, persists across reboots, accessible to all processes for that user. More secure for production servers. Harder to manage per-project. Best for production deployments and shared servers. Critical: The .env file must be added to .gitignore to prevent it from being committed to version control.

  3. Sample answer:

def extract_weather_summary(response_data):
    current = response_data.get("current_weather", {})
    temp_c = current.get("temperature", 0)
    wind_kmh = current.get("windspeed", 0)
    temp_f = temp_c * 9 / 5 + 32
    wind_mph = wind_kmh * 0.621371
    return (
        f"Currently {temp_c:.1f}°C ({temp_f:.1f}°F) "
        f"with wind at {wind_kmh:.1f} km/h ({wind_mph:.1f} mph)"
    )
  1. Problems: (1) Catches all Exception types indiscriminately — you cannot distinguish between a connection problem (no internet), an authentication failure (bad API key), a rate limit (should retry), and a programming error (bug in your code). Each of these requires a different response. (2) Returns None silently on error — callers have no way to know whether they got real data or a failure. (3) The error message "Something went wrong: {e}" is unhelpful to a user who needs to know what to do. Better version: Catch requests.exceptions.ConnectionError, requests.exceptions.Timeout, requests.exceptions.HTTPError separately; check response.status_code explicitly; raise meaningful exceptions with actionable messages; reserve bare except Exception only as a last resort with full logging.

Total points: 10 (Section A) + 5 (Section B) + 8 (Section C) + 12 (Section D) = 35 points

Suggested passing score: 28/35 (80%)